"""Respondline — proposal gating, human-gated execution, rejection."""

from __future__ import annotations

import pytest
from sqlalchemy import create_engine

from psyc import db
from psyc.lines import ledger as ledger_line
from psyc.lines import respond
from psyc.models import ActionStatus, ActionType, Outcome, Severity
from psyc.result import Ok
from conftest import make_case


@pytest.fixture
def fresh_db(tmp_path, monkeypatch):
    test_db = tmp_path / "test.db"
    eng = create_engine(f"sqlite:///{test_db}", future=True)
    db._metadata.create_all(eng, checkfirst=True)
    monkeypatch.setattr(db, "_engine", eng)
    monkeypatch.setattr(db, "DB_PATH", test_db)
    yield test_db


def test_low_severity_proposes_nothing(fresh_db):
    case = make_case(feed="urlhaus", ips=["1.2.3.4"], severity=Severity.LOW)
    db.upsert_case(case)
    assert respond.propose_for_case(case) == []


def test_high_severity_proposes_alert_and_blocklist(fresh_db):
    case = make_case(feed="feodo", ips=["9.9.9.9"], domains=["evil.com"], severity=Severity.HIGH)
    db.upsert_case(case)
    ids = respond.propose_for_case(case)
    assert len(ids) == 2
    actions = respond.list_actions(status=ActionStatus.PROPOSED)
    types = {a.action_type for a in actions}
    assert types == {ActionType.ALERT, ActionType.BLOCKLIST}


def test_proposal_is_idempotent_per_case(fresh_db):
    case = make_case(feed="feodo", ips=["9.9.9.9"], severity=Severity.CRITICAL)
    db.upsert_case(case)
    respond.propose_for_case(case)
    assert respond.propose_for_case(case) == []  # second call adds nothing
    assert respond.action_count(ActionStatus.PROPOSED) == 2


def test_blocklist_skipped_when_no_network_iocs(fresh_db):
    case = make_case(feed="malware-bazaar", hashes=["a" * 64], severity=Severity.HIGH)
    db.upsert_case(case)
    respond.propose_for_case(case)
    actions = respond.list_actions()
    # hash-only case → alert yes, blocklist no
    assert {a.action_type for a in actions} == {ActionType.ALERT}


def test_execute_fires_and_marks_executed(fresh_db, monkeypatch):
    case = make_case(feed="feodo", ips=["9.9.9.9"], severity=Severity.HIGH)
    db.upsert_case(case)
    aid = respond.propose_for_case(case)[0]

    captured = {}

    class _Resp:
        def raise_for_status(self): pass
        def json(self): return {"receipt_id": "MOCK-AB12"}

    class _Client:
        def __init__(self, *a, **k): pass
        def __enter__(self): return self
        def __exit__(self, *a): return False
        def post(self, url, json):
            captured["url"] = url
            captured["payload"] = json
            return _Resp()

    monkeypatch.setattr(respond.httpx, "Client", _Client)

    result = respond.execute_action(aid, approver="alice")
    assert isinstance(result, Ok)
    assert result.value.status is ActionStatus.EXECUTED
    assert result.value.approver == "alice"
    assert captured["payload"]["approved_by"] == "alice"
    # ledger has an ACTIONED row
    entries = ledger_line.list_by_case(case.case_id, limit=10)
    assert any(e.outcome is Outcome.ACTIONED for e in entries)


def test_execute_failure_marks_failed(fresh_db, monkeypatch):
    case = make_case(feed="feodo", ips=["9.9.9.9"], severity=Severity.HIGH)
    db.upsert_case(case)
    aid = respond.propose_for_case(case)[0]

    class _Client:
        def __init__(self, *a, **k): pass
        def __enter__(self): return self
        def __exit__(self, *a): return False
        def post(self, url, json): raise RuntimeError("sink down")

    monkeypatch.setattr(respond.httpx, "Client", _Client)
    result = respond.execute_action(aid)
    assert not isinstance(result, Ok)
    assert respond.get_action(aid).value.status is ActionStatus.FAILED


def test_reject_fires_nothing_and_records(fresh_db, monkeypatch):
    case = make_case(feed="feodo", ips=["9.9.9.9"], severity=Severity.HIGH)
    db.upsert_case(case)
    aid = respond.propose_for_case(case)[0]

    def boom(*a, **k):
        raise AssertionError("reject must not POST")
    monkeypatch.setattr(respond.httpx, "Client", boom)

    result = respond.reject_action(aid, approver="bob", reason="false positive")
    assert isinstance(result, Ok)
    a = respond.get_action(aid).value
    assert a.status is ActionStatus.REJECTED
    assert a.approver == "bob"
    entries = ledger_line.list_by_case(case.case_id, limit=10)
    assert any(e.outcome is Outcome.REJECTED and "false positive" in (e.detail or "") for e in entries)


def test_double_execute_refused(fresh_db, monkeypatch):
    case = make_case(feed="feodo", ips=["9.9.9.9"], severity=Severity.HIGH)
    db.upsert_case(case)
    aid = respond.propose_for_case(case)[0]

    class _Resp:
        def raise_for_status(self): pass
        def json(self): return {"receipt_id": "X"}

    class _Client:
        def __init__(self, *a, **k): pass
        def __enter__(self): return self
        def __exit__(self, *a): return False
        def post(self, url, json): return _Resp()

    monkeypatch.setattr(respond.httpx, "Client", _Client)
    respond.execute_action(aid)
    again = respond.execute_action(aid)
    assert not isinstance(again, Ok)  # already executed
