from __future__ import annotations

import json
from pathlib import Path

from recoil.pipeline.tools import linear_queue as lq


def _claim(**overrides: str) -> str:
    values = {
        "title": "Queue event is unstable",
        "evidence": "The `event_id` changes across identical captures.",
        "recommendation": "Derive ids from stable logical inputs.",
    }
    values.update(overrides)
    return lq.derive_claim_fingerprint(
        values["title"],
        values["evidence"],
        values["recommendation"],
    )


def _code_key(**overrides: str) -> str:
    values = {
        "category": "SSOT",
        "file": "recoil/pipeline/tools/linear_queue.py",
        "claim_signature": "derive_event_id",
    }
    values.update(overrides)
    return lq.derive_finding_key_code(
        values["category"],
        values["file"],
        values["claim_signature"],
    )


def _subject_key(**overrides: str) -> str:
    values = {
        "category": "untracked_work",
        "subject_kind": "pr",
        "subject_id": "https://github.com/example/recoil/pull/83",
        "claim_signature": "pr_token",
    }
    values.update(overrides)
    return lq.derive_finding_key_subject(
        values["category"],
        values["subject_kind"],
        values["subject_id"],
        values["claim_signature"],
    )


def _event(**overrides: object) -> dict:
    finding_key = _subject_key()
    values = {
        "schema_version": 1,
        "event_id": lq.derive_event_id(
            finding_key,
            "pr_untracked_work",
            "https://github.com/example/recoil/pull/83",
        ),
        "event_ts": "2026-06-03T00:00:00Z",
        "source_type": "pr_untracked_work",
        "session_id": None,
        "finding_key": finding_key,
        "category": "untracked_work",
        "subject_kind": "pr",
        "subject_id": "https://github.com/example/recoil/pull/83",
        "file": None,
        "title": "PR has no REC token",
        "evidence": "The PR branch has no REC-NN issue key.",
        "recommendation": "File one tracking issue for the PR.",
        "pr_url": "https://github.com/example/recoil/pull/83",
        "branch": "feature/no-token",
        "head_sha": None,
    }
    values.update(overrides)
    return dict(values)


def test_finding_key_code_stable_when_only_line_changes():
    assert _code_key() == _code_key()


def test_finding_key_code_changes_when_identity_inputs_change():
    base = _code_key()
    assert base != _code_key(category="bug")
    assert base != _code_key(file="recoil/pipeline/tools/other.py")
    assert base != _code_key(claim_signature="atomic_write_inbox")
    assert base == _code_key(title="Different title")


def test_finding_key_subject_stable_for_same_pr_url():
    assert _subject_key() == _subject_key()


def test_finding_key_subject_changes_when_pr_url_changes():
    assert _subject_key() != _subject_key(
        subject_id="https://github.com/example/recoil/pull/84"
    )


def test_finding_key_code_title_independent_and_distinguishing():
    base = lq.derive_finding_key_code("bug", "x.py", "asset_kind_dir")
    assert base == lq.derive_finding_key_code("bug", "x.py", "asset_kind_dir")
    assert base != lq.derive_finding_key_code("bug", "x.py", "resolve_ref")


def test_event_id_idempotent_and_atomic_write_overwrites_one_file(tmp_path):
    inbox = tmp_path / "inbox"
    event = _event()
    assert event["event_id"] == lq.derive_event_id(
        event["finding_key"],
        event["source_type"],
        event["pr_url"],
    )

    first_path = lq.atomic_write_inbox(event, inbox=inbox)
    second_path = lq.atomic_write_inbox(
        _event(evidence="The PR branch still has no REC-NN issue key."),
        inbox=inbox,
    )

    assert first_path == second_path
    assert sorted(path.name for path in inbox.glob("*.json")) == [f"{event['event_id']}.json"]
    assert json.loads(first_path.read_text(encoding="utf-8"))["evidence"].startswith(
        "The PR branch still"
    )


def test_build_event_derives_identity_for_manual_file_capture():
    event = lq.build_event(
        source_type="manual_file",
        category="bug",
        title="Queue event is unstable",
        evidence="The `event_id` changes across identical captures.",
        recommendation="Derive ids from stable logical inputs.",
        file="recoil/pipeline/tools/linear_queue.py",
        event_ts="2026-06-03T00:00:00Z",
    )

    finding_key = lq.derive_finding_key_code(
        event["category"],
        event["file"],
        lq.effective_claim_signature(event["evidence"], None),
    )

    assert event["finding_key"] == finding_key
    assert event["event_id"] == lq.derive_event_id(
        finding_key,
        "manual_file",
        f"{finding_key}|2026-06-03|",
    )
    assert not lq.validate_event(event)


def test_build_event_accepts_nightwatch_source_type():
    event = lq.build_event(
        source_type="nightwatch",
        category="bug",
        title="Queue event is unstable",
        evidence="The `event_id` changes across identical captures.",
        recommendation="Derive ids from stable logical inputs.",
        file="recoil/pipeline/tools/linear_queue.py",
        normalized_anchor="def build_event",
        law_or_rule="SSOT-1",
        event_ts="2026-06-03T00:00:00Z",
    )

    assert event["source_type"] == "nightwatch"
    assert not lq.validate_event(event)


def test_build_event_finding_key_title_independent_and_distinguishing():
    first = lq.build_event(
        source_type="nightwatch",
        category="bug",
        title="First phrasing",
        evidence="The `asset_kind_dir` call raises.",
        recommendation="Use canonical paths.",
        file="recoil/pipeline/tools/dispatch_cli.py",
        normalized_anchor="anchor-a",
    )
    second = lq.build_event(
        source_type="nightwatch",
        category="bug",
        title="Second phrasing",
        evidence="Later prose still cites `asset_kind_dir`.",
        recommendation="Resolve through the canonical API.",
        file="recoil/pipeline/tools/dispatch_cli.py",
        normalized_anchor="anchor-a",
    )
    third = lq.build_event(
        source_type="nightwatch",
        category="bug",
        title="Different claim",
        evidence="The `resolve_entity_refs` call bypasses the resolver.",
        recommendation="Use canonical paths.",
        file="recoil/pipeline/tools/dispatch_cli.py",
        normalized_anchor="anchor-a",
    )

    assert first["finding_key"] == second["finding_key"]
    assert first["finding_key"] != third["finding_key"]


def test_empty_evidence_finding_key_falls_back_to_normalized_anchor():
    subject_a = lq.build_event(
        source_type="nightwatch",
        category="bug",
        subject_kind="repo",
        subject_id="recoil",
        title="A",
        evidence="",
        recommendation="r",
        normalized_anchor="def alpha",
    )
    subject_b = lq.build_event(
        source_type="nightwatch",
        category="bug",
        subject_kind="repo",
        subject_id="recoil",
        title="B",
        evidence="",
        recommendation="r",
        normalized_anchor="def beta",
    )
    subject_missing = lq.build_event(
        source_type="nightwatch",
        category="bug",
        subject_kind="repo",
        subject_id="recoil",
        title="C",
        evidence="",
        recommendation="r",
        normalized_anchor="anchor_not_found",
    )
    assert subject_a["finding_key"] != subject_b["finding_key"]
    assert subject_a["finding_key"] != subject_missing["finding_key"]

    file_a = lq.build_event(
        source_type="nightwatch",
        category="bug",
        title="A",
        evidence="",
        recommendation="r",
        file="x.py",
        normalized_anchor="def alpha",
    )
    file_b = lq.build_event(
        source_type="nightwatch",
        category="bug",
        title="B",
        evidence="",
        recommendation="r",
        file="x.py",
        normalized_anchor="def beta",
    )
    assert file_a["finding_key"] != file_b["finding_key"]

    no_locus_a = lq.build_event(
        source_type="nightwatch",
        category="bug",
        title="A",
        evidence="",
        recommendation="r",
        normalized_anchor="def alpha",
    )
    no_locus_b = lq.build_event(
        source_type="nightwatch",
        category="bug",
        title="B",
        evidence="",
        recommendation="r",
        normalized_anchor="def beta",
    )
    assert no_locus_a["finding_key"] != no_locus_b["finding_key"]


def test_no_locus_content_free_findings_share_key():
    # Characterization of the disclosed lossy-projection boundary under the OFF
    # default: content-free no-locus findings are validly keyed/enqueued, but
    # share a bucket because there is no stable distinguishing content.
    first = lq.build_event(
        source_type="nightwatch",
        category="bug",
        title="A",
        evidence="",
        recommendation="r",
        normalized_anchor="",
    )
    second = lq.build_event(
        source_type="nightwatch",
        category="bug",
        title="B",
        evidence="",
        recommendation="r",
        normalized_anchor="anchor_not_found",
    )
    assert first["finding_key"].startswith("sha256:")
    assert second["finding_key"].startswith("sha256:")
    assert first["finding_key"] == second["finding_key"]


def test_build_event_derives_identity_for_pr_capture_from_pr_url():
    event = lq.build_event(
        source_type="pr_untracked_work",
        category="untracked_work",
        subject_kind="pr",
        subject_id="https://github.com/example/recoil/pull/83",
        title="Untracked work: feature/no-token",
        evidence="PR URL: https://github.com/example/recoil/pull/83",
        recommendation="Link this PR to a REC issue or open a tracking issue.",
        pr_url="https://github.com/example/recoil/pull/83",
        branch="feature/no-token",
    )

    assert event["finding_key"] == lq.derive_finding_key_subject(
        event["category"],
        event["subject_kind"],
        event["subject_id"],
        lq.effective_claim_signature(event["evidence"], None),
    )
    assert event["event_id"] == lq.derive_event_id(
        event["finding_key"],
        event["source_type"],
        event["pr_url"],
    )
    assert not lq.validate_event(event)


def test_atomic_write_inbox_then_read_inbox_round_trips_and_flags_malformed(tmp_path):
    inbox = tmp_path / "inbox"
    event = _event()
    lq.atomic_write_inbox(event, inbox=inbox)
    malformed = inbox / "malformed.json"
    malformed.write_text("{not-json", encoding="utf-8")

    items = lq.read_inbox(inbox=inbox)

    good = [item for item in items if isinstance(item, dict)]
    bad = [item for item in items if isinstance(item, tuple)]
    assert good == [event]
    assert len(bad) == 1
    assert bad[0][0] == str(malformed)
    assert bad[0][1]


def test_validate_event_rejects_unknown_top_level_keys():
    event = _event(extra="nope")
    problems = lq.validate_event(event)
    assert any("unknown top-level keys" in problem for problem in problems)


def test_validate_event_rejects_missing_required_keys():
    event = _event()
    del event["recommendation"]
    problems = lq.validate_event(event)
    assert any("missing required keys" in problem for problem in problems)


def test_validate_event_rejects_structured_evidence_and_recommendation():
    problems = lq.validate_event(_event(evidence={"do": "not execute"}))
    assert "evidence must be a string" in problems

    problems = lq.validate_event(_event(recommendation=["do", "not", "execute"]))
    assert "recommendation must be a string" in problems


def test_append_ledger_latest_state_and_already_handled(tmp_path):
    ledger = tmp_path / "linear-queue" / "ledger.jsonl"
    key = _subject_key()
    sentinel = {"sentinel": True}
    ledger.parent.mkdir(parents=True, exist_ok=True)
    with open(ledger, "w", encoding="utf-8") as fh:
        fh.write(json.dumps(sentinel) + "\n")

    pending = {
        "schema_version": 1,
        "ledger_ts": "2026-06-03T00:00:00Z",
        "finding_key": key,
        "event_id": "event-a",
        "state": "pending",
        "linear_issue": None,
        "reason": None,
    }
    filed = {
        **pending,
        "ledger_ts": "2026-06-03T00:01:00Z",
        "state": "filed",
        "linear_issue": "REC-99",
    }

    lq.append_ledger(pending, ledger=ledger)
    rows = lq.load_ledger(ledger=ledger)
    assert rows == [sentinel, pending]
    assert lq.latest_ledger_state(key, rows) == "pending"
    assert not lq.is_already_handled(key, rows)

    lq.append_ledger(filed, ledger=ledger)
    rows = lq.load_ledger(ledger=ledger)
    assert rows == [sentinel, pending, filed]
    assert lq.latest_ledger_state(key, rows) == "filed"
    assert lq.is_already_handled(key, rows)

    for line in ledger.read_text(encoding="utf-8").splitlines():
        json.loads(line)


def test_move_to_done_removes_from_inbox_and_lands_in_done(tmp_path):
    inbox = tmp_path / "inbox"
    done = tmp_path / "done"
    event = _event()
    inbox_path = lq.atomic_write_inbox(event, inbox=inbox)

    lq.move_to_done(event["event_id"], inbox=inbox, done=done)

    done_path = done / inbox_path.name
    assert not inbox_path.exists()
    assert done_path.exists()
    assert json.loads(done_path.read_text(encoding="utf-8")) == event


def test_local_smoke_manual_file_path_collapses_duplicate_events(tmp_path):
    inbox = tmp_path / "inbox"
    first = lq.build_event(
        source_type="manual_file",
        category="bug",
        title="A",
        evidence="e1",
        recommendation="r",
        file="x.py",
    )
    second = lq.build_event(
        source_type="manual_file",
        category="drift",
        title="B",
        evidence="e2",
        recommendation="r",
        file="y.py",
    )
    duplicate_first = lq.build_event(
        source_type="manual_file",
        category="bug",
        title="A",
        evidence="e1",
        recommendation="r",
        file="x.py",
    )

    for event in (first, second, duplicate_first):
        lq.atomic_write_inbox(event, inbox=inbox)

    files = list(inbox.glob("*.json"))
    assert len(files) == 2
    assert sorted(event["event_id"] for event in lq.read_inbox(inbox=inbox)) == sorted(
        [first["event_id"], second["event_id"]]
    )


def test_local_smoke_ledger_dedup_oracle(tmp_path):
    ledger = tmp_path / "ledger.jsonl"
    key = "sha256:deadbeef"

    lq.append_ledger(
        {
            "schema_version": 1,
            "finding_key": key,
            "event_id": "e",
            "state": "pending",
            "linear_issue": None,
            "reason": None,
        },
        ledger=ledger,
    )
    lq.append_ledger(
        {
            "schema_version": 1,
            "finding_key": key,
            "event_id": "e",
            "state": "filed",
            "linear_issue": "REC-99",
            "reason": None,
        },
        ledger=ledger,
    )

    assert lq.is_already_handled(key, lq.load_ledger(ledger=ledger)) is True


def test_linear_queue_module_has_no_network_linear_or_process_calls():
    source = Path(lq.__file__).read_text(encoding="utf-8")
    forbidden = [
        "mcp__linear__",
        "requests",
        "urllib",
        "http",
        "LINEAR_API_KEY",
        "subprocess",
    ]

    assert not [term for term in forbidden if term in source]


def test_ledger_writes_remain_append_only_in_source():
    source = Path(lq.__file__).read_text(encoding="utf-8")
    forbidden = ["write_text", "truncate", 'mode="w"', "mode='w'"]

    assert not [term for term in forbidden if term in source]
    assert 'open(ledger, "a"' in source
