"""Tests for scripts/kb_proposal_normalize.py."""

from __future__ import annotations

import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT / "scripts"))

import kb_proposal_normalize as norm  # noqa: E402


def test_rich_claim_split_stays_atomic_when_one_fragment_is_blocked():
    preview = norm.normalize_proposal(
        {
            "id": "p-freeform",
            "proposal_type": "attach_evidence",
            "status": "approved",
            "payload": {
                "old_claim": {"id": "d0000000-0000-0000-0000-000000000005"},
                "claim_candidates": [
                    {
                        "claim_key": "new_claim_key",
                        "headline": "New claim headline",
                        "edges": [
                            {
                                "edge_type": "supersedes_component_of",
                                "target_claim_id": "d0000000-0000-0000-0000-000000000005",
                            }
                        ],
                        "evidence": [{"source_key": "human_review"}],
                    }
                ],
                "source_candidates": [{"source_key": "human_review"}],
            },
        }
    )

    assert preview["normalization_state"] == "blocked_rich_normalization"
    assert preview["strict_child_count"] == 0
    assert preview["strict_child_proposals"] == []
    assert preview["blocked_count"] == 1
    assert "unsupported edge_type" in preview["blocked_fragments"][0]["reason"]
    assert "Resolve every blocked rich fragment" in preview["next_normalization_action"]


def test_exact_edge_fragment_emits_strict_add_edge_child():
    preview = norm.normalize_proposal(
        {
            "id": "p-edge",
            "proposal_type": "attach_evidence",
            "status": "approved",
            "payload": {
                "proposed_supersession_edges": [
                    {
                        "from_claim": "11111111-1111-1111-1111-111111111111",
                        "to_claim_id": "22222222-2222-2222-2222-222222222222",
                        "edge_type": "supports",
                        "weight": 0.7,
                    }
                ]
            },
        }
    )

    assert preview["normalization_state"] == "strict_children_ready"
    assert preview["blocked_count"] == 0
    assert preview["strict_child_count"] == 1
    child = preview["strict_child_proposals"][0]
    assert child["proposal_type"] == "add_edge"
    assert child["payload"]["apply_payload"] == {
        "from_claim": "11111111-1111-1111-1111-111111111111",
        "to_claim": "22222222-2222-2222-2222-222222222222",
        "edge_type": "supports",
        "weight": 0.7,
    }


def test_exact_evidence_fragments_emit_one_attach_evidence_child():
    preview = norm.normalize_proposal(
        {
            "id": "p-evidence",
            "proposal_type": "attach_evidence",
            "status": "approved",
            "payload": {
                "evidence": [
                    {
                        "claim_id": "33333333-3333-3333-3333-333333333333",
                        "source_id": "44444444-4444-4444-4444-444444444444",
                        "role": "grounds",
                        "weight": 0.9,
                    }
                ]
            },
        }
    )

    assert preview["normalization_state"] == "strict_children_ready"
    assert preview["strict_child_count"] == 1
    child = preview["strict_child_proposals"][0]
    assert child["proposal_type"] == "attach_evidence"
    assert child["payload"]["apply_payload"]["evidence"] == [
        {
            "claim_id": "33333333-3333-3333-3333-333333333333",
            "source_id": "44444444-4444-4444-4444-444444444444",
            "role": "grounds",
            "weight": 0.9,
        }
    ]


def test_existing_apply_payload_is_already_strict():
    preview = norm.normalize_proposal(
        {
            "id": "p-strict",
            "proposal_type": "add_edge",
            "status": "approved",
            "payload": {
                "apply_payload": {
                    "from_claim": "11111111-1111-1111-1111-111111111111",
                    "to_claim": "22222222-2222-2222-2222-222222222222",
                    "edge_type": "supports",
                }
            },
        }
    )

    assert preview["normalization_state"] == "already_strict_apply_payload"
    assert preview["strict_child_count"] == 0
    assert preview["blocked_count"] == 0


def test_rich_claim_graph_normalizes_to_one_atomic_approve_claim_bundle():
    preview = norm.normalize_proposal(
        {
            "id": "77777777-7777-4777-8777-777777777777",
            "proposal_type": "attach_evidence",
            "status": "approved",
            "proposed_by_agent_id": "11111111-1111-4111-8111-111111111111",
            "source_ref": "telegram:test:approved",
            "payload": {
                "claim_candidates": [
                    {
                        "claim_key": "canonical_test_claim",
                        "headline": "A reviewed claim becomes canonical after approval.",
                        "type": "structural",
                        "confidence": 0.8,
                        "tags": ["working-leo"],
                        "evidence": [{"source_key": "canonical_test_source", "role": "grounds"}],
                    }
                ],
                "proposed_claims": [
                    {
                        "claim_key": "second_canonical_test_claim",
                        "text": "A proposed_claims item is normalized too.",
                        "type": "normative",
                    }
                ],
                "source_candidates": [
                    {
                        "source_key": "canonical_test_source",
                        "source_type": "paper",
                        "url": "https://example.test/canonical",
                        "key_excerpt": "Reviewed source excerpt.",
                    }
                ],
            },
        }
    )

    assert preview["normalization_state"] == "strict_children_ready"
    assert preview["blocked_count"] == 0
    assert preview["strict_child_count"] == 1
    child = preview["strict_child_proposals"][0]
    assert child["proposal_type"] == "approve_claim"
    bundle = child["payload"]["apply_payload"]
    assert bundle["agent_id"] == "11111111-1111-4111-8111-111111111111"
    assert len(bundle["claims"]) == 2
    assert len(bundle["sources"]) == 3  # reviewed source plus two proposal-body sources
    assert len(bundle["evidence"]) == 3
    assert bundle["edges"] == []
    accounting = bundle["normalization_manifest"]["candidate_accounting"]
    assert accounting["input_collections"] == {
        "claim_candidates": 1,
        "proposed_claims": 1,
        "source_candidates": 1,
    }
    assert accounting["claim_candidates"]["input_count"] == 2
    assert accounting["claim_candidates"]["planned_row_count"] == 2
    assert accounting["source_candidates"]["input_count"] == 1
    assert accounting["source_candidates"]["planned_row_count"] == 1
    assert accounting["generated_claim_body_sources"]["planned_row_count"] == 2
    assert accounting["total_planned_source_rows"] == 3


def test_source_content_sha256_is_preserved_as_the_canonical_source_hash():
    content_hash = "ab" * 32
    preview = norm.normalize_proposal(
        {
            "id": "77777777-7777-4777-8777-777777777779",
            "proposal_type": "attach_evidence",
            "status": "pending_review",
            "payload": {
                "claim_candidates": [
                    {
                        "claim_key": "hash_bound_claim",
                        "text": "The claim is bound to source bytes.",
                        "evidence": [{"source_key": "hash_bound_source", "role": "grounds"}],
                    }
                ],
                "source_candidates": [
                    {
                        "source_key": "hash_bound_source",
                        "source_type": "document",
                        "storage_path": "/evidence/source.txt",
                        "key_excerpt": "The claim is bound to source bytes.",
                        "content_sha256": content_hash.upper(),
                    }
                ],
            },
        }
    )

    assert preview["normalization_state"] == "strict_children_ready"
    source_rows = preview["strict_child_proposals"][0]["payload"]["apply_payload"]["sources"]
    source = next(row for row in source_rows if row["storage_path"] == "/evidence/source.txt")
    assert source["hash"] == content_hash
    assert content_hash.upper() in source["excerpt"]


def test_invalid_source_content_hash_blocks_rich_normalization():
    preview = norm.normalize_proposal(
        {
            "id": "77777777-7777-4777-8777-777777777780",
            "proposal_type": "attach_evidence",
            "status": "pending_review",
            "payload": {
                "claim_candidates": [
                    {
                        "claim_key": "bad_hash_claim",
                        "text": "This must not normalize.",
                        "evidence": [{"source_key": "bad_hash_source"}],
                    }
                ],
                "source_candidates": [
                    {
                        "source_key": "bad_hash_source",
                        "key_excerpt": "This must not normalize.",
                        "content_sha256": "not-a-sha256",
                    }
                ],
            },
        }
    )

    assert preview["normalization_state"] == "blocked_lossy_rich_normalization"
    assert preview["strict_child_count"] == 0
    assert any("invalid content_sha256" in item["reason"] for item in preview["blocked_fragments"])


def test_rich_normalization_blocks_malformed_or_duplicate_candidates():
    proposal = {
        "id": "77777777-7777-4777-8777-777777777777",
        "proposal_type": "attach_evidence",
        "status": "approved",
        "payload": {
            "claim_candidates": [
                {"claim_key": "duplicate", "text": "first"},
                {"claim_key": "duplicate", "text": "second"},
                {"text": "missing key"},
                "not-an-object",
            ],
            "proposed_claims": [
                {"claim_key": "duplicate", "text": "duplicate across claim collections"},
                {"claim_key": "   ", "text": "blank key"},
                42,
            ],
            "source_candidates": [
                {"source_key": "source", "source_type": "paper"},
                {"source_key": "source", "source_type": "paper"},
            ],
        },
    }
    preview = norm.normalize_proposal(proposal)
    assert preview["normalization_state"] == "blocked_lossy_rich_normalization"
    assert preview["strict_child_count"] == 0
    assert preview["strict_child_proposals"] == []
    reasons = " ".join(item["reason"] for item in preview["blocked_fragments"])
    assert "duplicate" in reasons
    assert "missing" in reasons
    assert "object" in reasons


def test_rich_normalization_retains_type_mapping_manifest_for_review():
    preview = norm.normalize_proposal(
        {
            "id": "77777777-7777-4777-8777-777777777778",
            "proposal_type": "attach_evidence",
            "status": "approved",
            "payload": {
                "claim_candidates": [
                    {"claim_key": "governance_claim", "text": "Governance process.", "type": "governance"},
                    {"claim_key": "typed_claim", "text": "Typed detail.", "type": "normative/detail"},
                ],
                "source_candidates": [
                    {"source_key": "book_source", "source_type": "book", "key_excerpt": "Excerpt"},
                    {"source_key": "web_source", "url": "https://example.test/source"},
                ],
            },
        }
    )
    child = preview["strict_child_proposals"][0]
    manifest = child["payload"]["apply_payload"]["normalization_manifest"]
    assert {(item["claim_key"], item["from"], item["to"]) for item in manifest["claim_type_mappings"]} == {
        ("governance_claim", "governance", "meta"),
        ("typed_claim", "normative/detail", "normative"),
    }
    assert {(item["source_key"], item["from"], item["to"]) for item in manifest["source_type_mappings"]} == {
        ("book_source", "book", "other"),
        ("web_source", "url", "article"),
        ("governance_claim__proposal_body", "proposal_body", "other"),
        ("typed_claim__proposal_body", "proposal_body", "other"),
    }


def test_rich_normalization_rejects_non_list_candidate_collection():
    preview = norm.normalize_proposal(
        {
            "id": "77777777-7777-4777-8777-777777777779",
            "proposal_type": "attach_evidence",
            "payload": {"claim_candidates": {"claim_key": "not-in-a-list"}},
        }
    )

    assert preview["normalization_state"] == "blocked_lossy_rich_normalization"
    assert preview["strict_child_proposals"] == []
    assert preview["blocked_fragments"][0]["location"] == "payload.claim_candidates"
    assert "list of objects" in preview["blocked_fragments"][0]["reason"]


def test_rich_normalization_fails_closed_when_planner_skips_candidate(monkeypatch):
    proposal = {
        "id": "77777777-7777-4777-8777-777777777780",
        "proposal_type": "attach_evidence",
        "payload": {
            "claim_candidates": [{"claim_key": "must_be_planned", "text": "Keep me."}],
            "source_candidates": [{"source_key": "must_be_planned_too", "source_type": "paper"}],
        },
    }
    lossy_plan = norm.rich_plan.build_creation_plan(proposal)
    lossy_plan["planned_rows"]["claims"] = []
    monkeypatch.setattr(norm.rich_plan, "build_creation_plan", lambda *_args, **_kwargs: lossy_plan)

    preview = norm.normalize_proposal(proposal)

    assert preview["normalization_state"] == "blocked_lossy_rich_normalization"
    assert preview["strict_child_proposals"] == []
    assert preview["strict_child_count"] == 0
    assert any("claim rows" in item["reason"] for item in preview["blocked_fragments"])


def test_source_candidates_without_claims_are_not_silently_dropped():
    preview = norm.normalize_proposal(
        {
            "id": "77777777-7777-4777-8777-777777777781",
            "proposal_type": "attach_evidence",
            "payload": {
                "source_candidates": [
                    {"source_key": "orphan_source", "source_type": "paper", "key_excerpt": "Keep me."}
                ]
            },
        }
    )

    assert preview["normalization_state"] == "blocked_lossy_rich_normalization"
    assert preview["strict_child_proposals"] == []
    assert "without at least one claim candidate" in preview["blocked_fragments"][0]["reason"]
