"""Rejections, charges, shown ids, and the abort/truncate line."""

from __future__ import annotations

import json

from alert_triage.engine import Engine
from alert_triage.generator import MAX_TURNS, world_for_seed
from alert_triage.reward import DOC_MINUTES, LOOKUP_MINUTES, SCREEN_MINUTES, TURN_MINUTES, score

JUNK = ["", "   ", None, "no json here", "{", "}", "{}", "[]", "null", "{\"action\":null}", "{\"action\":\"lookup\"}",
        "{\"action\":\"lookup\",\"what\":\"history\"}", "{\"action\":\"lookup\",\"what\":\"history\",\"month\":7}",
        "{\"action\":\"lookup\",\"what\":\"counterparty\"}", "{\"action\":\"lookup\",\"what\":\"counterparty\",\"id\":[]}",
        "{\"action\":\"close\"}", "{\"action\":\"close\",\"reason\":\"RULE_ARTEFACT\"}",
        "{\"action\":\"close\",\"reason\":\"RULE_ARTEFACT\",\"cites\":{}}", "{\"action\":\"escalate\",\"cites\":[\"kyc.country\"]}",
        "{\"action\":\"lookup\",\"what\":\"documents\",\"x\":NaN}", "\x00\x01", "{" * 50]


def _e(seed: int = 0) -> Engine:
    return Engine(world_for_seed(seed))


def test_screen_costs_and_first_turn() -> None:
    e = _e()
    assert e.minutes == SCREEN_MINUTES and e.turns == 0 and not e.done


def test_junk_never_raises_and_costs_one_turn_each() -> None:
    for junk in JUNK:
        e = _e()
        step = e.step(junk)
        assert not step.accepted and step.reason and e.turns == 1 and e.rejected == 1
        assert e.minutes == SCREEN_MINUTES + TURN_MINUTES


def test_trailing_garbage_after_a_balanced_object_is_fine() -> None:
    e = _e()
    assert e.step('{"action":"lookup","what":"documents"}' + "}" * 50 + " and more prose").accepted


def test_lookup_charges_its_price_only_when_accepted() -> None:
    e = _e()
    e.step('{"action":"lookup","what":"documents"}')
    assert e.minutes == SCREEN_MINUTES + TURN_MINUTES + DOC_MINUTES
    e.step('{"action":"lookup","what":"documents"}')       # repeat: rejected
    assert e.minutes == SCREEN_MINUTES + 2 * TURN_MINUTES + DOC_MINUTES and e.rejected == 1
    e.step('{"action":"lookup","what":"prior_alerts"}')
    assert e.minutes == SCREEN_MINUTES + 3 * TURN_MINUTES + DOC_MINUTES + LOOKUP_MINUTES


def test_history_window_is_the_twelve_months_ending_in_the_fire_month() -> None:
    e = _e()
    months = e.world["months"]
    assert len(months) == 12 and months[-1] == e.world["alert"]["fired"][:7]
    assert e.step(json.dumps({"action": "lookup", "what": "history", "month": months[0]})).accepted
    before = months[0][:4] + "-" + f"{int(months[0][5:]) - 1:02d}" if months[0][5:] != "01" else f"{int(months[0][:4]) - 1}-12"
    assert not e.step(json.dumps({"action": "lookup", "what": "history", "month": before})).accepted


def test_counterparty_must_have_been_shown() -> None:
    e = _e()
    on_screen = e.world["screen_counterparties"][0]["id"]
    assert e.step(json.dumps({"action": "lookup", "what": "counterparty", "id": on_screen})).accepted
    assert not e.step('{"action":"lookup","what":"counterparty","id":"CP-999"}').accepted
    assert not e.step(json.dumps({"action": "lookup", "what": "counterparty", "id": on_screen})).accepted


def test_cites_must_be_shown_and_are_deduplicated() -> None:
    e = _e()
    step = e.step('{"action":"close","reason":"RULE_ARTEFACT","cites":["kyc.country","doc.D-0000"]}')
    assert not step.accepted and "not been shown" in step.reason
    step = e.step('{"action":"close","reason":"RULE_ARTEFACT","cites":["kyc.country","kyc.country","kyc.pep"]}')
    assert step.accepted and step.action["cites"] == ["kyc.country", "kyc.pep"] and e.done


def test_documents_become_citable_after_the_lookup() -> None:
    e = _e()
    doc = e.world["documents"][0]["id"]
    assert not e.step(json.dumps({"action": "close", "reason": "DOCUMENTED_SOURCE_OF_FUNDS", "cites": [doc]})).accepted
    assert e.step('{"action":"lookup","what":"documents"}').accepted
    assert e.step(json.dumps({"action": "close", "reason": "DOCUMENTED_SOURCE_OF_FUNDS", "cites": [doc]})).accepted


def test_enums_are_exact_case() -> None:
    e = _e()
    assert not e.step('{"action":"close","reason":"rule_artefact","cites":["kyc.country"]}').accepted
    assert not e.step('{"action":"ESCALATE","typology":"UNKNOWN","cites":["kyc.country"]}').accepted
    assert e.step('{"action":"escalate","typology":"UNKNOWN","cites":["kyc.country"]}').accepted


def test_eight_rejections_abort_and_score_zero() -> None:
    e = _e()
    for _ in range(MAX_TURNS):
        e.step("nothing")
    assert e.done and e.aborted and e.outcome == "aborted"
    assert e.step("{}").reason == "the episode is over" and e.turns == MAX_TURNS
    assert score(e.episode()) == {"caught": 0.0, "hours": 0.0, "evidence": 0.0}


def test_a_disposition_on_the_eighth_turn_is_accepted() -> None:
    e = _e()
    for _ in range(MAX_TURNS - 1):
        e.step("nothing")
    assert e.step('{"action":"close","reason":"RULE_ARTEFACT","cites":["kyc.country"]}').accepted
    assert e.outcome in ("solved", "failed")


def test_stopping_early_is_truncated_not_aborted() -> None:
    e = _e()
    e.step("nothing")
    e.step('{"action":"lookup","what":"documents"}')
    assert not e.done and e.outcome is None
    ep = e.episode()
    assert ep.truncated
    assert score(ep) == {"caught": None, "hours": None, "evidence": None}


def test_view_never_shows_hidden_fields() -> None:
    e = _e(5)
    e.step('{"action":"lookup","what":"documents"}')
    e.step('{"action":"lookup","what":"prior_alerts"}')
    text = json.dumps(e.view())
    for key in ("label", "planted", "typology", "overlay", "tier", "template", "reference_minutes", "reference_policy"):
        assert f'"{key}"' not in text, key
