from __future__ import annotations

import hashlib
import importlib.util
import json
import re
import stat
import subprocess
from pathlib import Path
from types import SimpleNamespace

import pytest

ROOT = Path(__file__).resolve().parents[1]
KB_TOOL = ROOT / "hermes-agent" / "leoclean-bin" / "kb_tool.py"


def load_module():
    spec = importlib.util.spec_from_file_location("kb_tool_propose_source", KB_TOOL)
    assert spec and spec.loader
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


def source_bundle(module) -> dict:
    payload = {
        "apply_payload": {
            "contract_version": 2,
            "claims": [],
            "sources": [],
            "evidence": [],
            "edges": [],
            "reasoning_tools": [],
        }
    }
    payload_sha256 = module.canonical_json_sha256(payload)
    child = {
        "id": "3cf4f2de-ea69-4e3b-a7ec-128b604d4f08",
        "proposal_type": "approve_claim",
        "status": "pending_review",
        "proposed_by_handle": "leo",
        "proposed_by_agent_id": None,
        "channel": "source_document_compiler",
        "source_ref": "normalized:source-parent:0123456789abcdef",
        "rationale": "Strict normalized child of one hash-bound source packet.",
        "payload": payload,
        "payload_sha256": payload_sha256,
    }
    bundle = {
        "schema": module.SOURCE_COMPILER_BUNDLE_SCHEMA,
        "status": "pending_review",
        "build_only": True,
        "database_write_performed": False,
        "canonical_apply_performed": False,
        "strict_child_proposal": child,
        "validated_manifest": {
            "artifact_sha256": "a" * 64,
            "extracted_text_sha256": "b" * 64,
            "source": {
                "identity": "document:sha256:" + "a" * 64,
                "locator": "artifact://sha256/" + "a" * 64,
            }
        },
        "hashes": {
            "artifact_sha256": "a" * 64,
            "extracted_text_sha256": "b" * 64,
            "manifest_canonical_sha256": "c" * 64,
            "strict_child_payload_sha256": payload_sha256,
        },
    }
    bundle["hashes"]["bundle_content_sha256"] = module.canonical_json_sha256(bundle)
    return bundle


def status_row(proposal_count: int) -> dict:
    return {
        "artifact": "teleo_kb_status",
        "backend": "teleo|postgres",
        "high_signal_rows": {
            "claims": 1837,
            "sources": 4145,
            "claim_edges": 4916,
            "claim_evidence": 4670,
            "kb_proposals": proposal_count,
        },
        "proposal_status_counts": {"pending_review": 14},
    }


def test_propose_source_stages_only_pending_review_and_writes_private_receipt(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    module = load_module()
    bundle = source_bundle(module)
    child = bundle["strict_child_proposal"]
    compiler = tmp_path / "compile_kb_source_packet.py"
    compiler.write_text("# compiler fixture\n", encoding="utf-8")
    monkeypatch.setattr(module, "SOURCE_COMPILER_PATH", compiler)
    monkeypatch.setattr(module, "compile_source_bundle", lambda _args: bundle)

    calls: list[str] = []
    status_reads = iter((status_row(26), status_row(27)))

    def fake_psql_json(_args, sql):
        calls.append(sql)
        if "insert into kb_stage.kb_proposals" not in sql:
            return [next(status_reads)]
        return [
            {
                "created": True,
                "proposal_id": child["id"],
                "proposal_type": child["proposal_type"],
                "status": "pending_review",
                "source_ref": child["source_ref"],
                "payload": child["payload"],
                "payload_sha256": child["payload_sha256"],
                "created_at": "2026-07-13T18:00:00+00:00",
                "reviewed_at": None,
                "applied_at": None,
            }
        ]

    monkeypatch.setattr(module, "psql_json", fake_psql_json)
    receipt_path = tmp_path / "receipts" / "source.json"
    args = SimpleNamespace(
        local=True,
        artifact=tmp_path / "source.pdf",
        text=tmp_path / "source.txt",
        manifest=tmp_path / "source-manifest.json",
        receipt=receipt_path,
    )

    receipt = module.propose_source(args)

    assert receipt["status"] == "pending_review"
    assert receipt["created"] is True
    assert receipt["proposal"]["id"] == child["id"]
    assert receipt["database"]["canonical_counts_unchanged"] is True
    assert receipt["database"]["proposal_count_delta"] == 1
    assert receipt["database"]["proposal_count_matches_command"] is True
    assert receipt["write_contract"] == {
        "target_table": "kb_stage.kb_proposals",
        "database_write_performed_by_command": True,
        "canonical_apply_performed": False,
        "production_db_apply_ran": False,
        "public_table_write_path_present": False,
    }
    assert json.loads(receipt_path.read_text(encoding="utf-8")) == receipt
    assert stat.S_IMODE(receipt_path.stat().st_mode) == 0o600

    stage_sql = next(sql for sql in calls if "insert into kb_stage.kb_proposals" in sql)
    assert "on conflict (id) do nothing" in stage_sql
    assert "marker_count <> 1 or exact_count <> 1" in stage_sql
    assert not re.search(r"\b(?:insert|update|delete)\s+(?:into\s+|from\s+)?public\.", stage_sql, re.I)
    assert len(calls) == 3


def test_source_bundle_rejects_tampered_child_payload() -> None:
    module = load_module()
    bundle = source_bundle(module)
    bundle["strict_child_proposal"]["payload"]["apply_payload"]["claims"].append({"id": "tampered"})

    with pytest.raises(SystemExit, match="payload hash does not match"):
        module._validated_source_bundle(bundle)


def test_deployed_bridge_accepts_real_source_compiler_bundle(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    module = load_module()
    artifact_bytes = b"%PDF-1.7\nbridge compatibility fixture\n%%EOF\n"
    extracted_text = (
        "Bridge compatibility source.\n\n"
        "Prepared source inputs may become a pending review proposal.\n\n"
        "Canonical rows still require review and guarded apply."
    )
    artifact = tmp_path / "source.pdf"
    text = tmp_path / "source.txt"
    manifest_path = tmp_path / "source-manifest.json"
    artifact.write_bytes(artifact_bytes)
    text.write_text(extracted_text, encoding="utf-8")
    artifact_sha256 = hashlib.sha256(artifact_bytes).hexdigest()
    text_sha256 = hashlib.sha256(extracted_text.encode()).hexdigest()
    manifest = {
        "schema": "livingip.kbSourceExtractionManifest.v1",
        "artifact_sha256": artifact_sha256,
        "extracted_text_sha256": text_sha256,
        "extractor": {"name": "bridge-test", "version": "1"},
        "source": {
            "identity": f"document:sha256:{artifact_sha256}",
            "source_key": "bridge_compatibility_source",
            "source_type": "article",
            "title": "Bridge compatibility source",
            "locator": f"artifact://sha256/{artifact_sha256}",
        },
        "claims": [
            {
                "claim_key": "prepared_input_staging",
                "type": "structural",
                "text": "Prepared inputs can be staged for review without becoming canonical.",
                "quote": "Prepared source inputs may become a pending review proposal.",
                "confidence": 0.99,
                "tags": ["source-intake"],
                "evidence": [
                    {
                        "quote": "Canonical rows still require review and guarded apply.",
                        "role": "grounds",
                    }
                ],
            }
        ],
    }
    manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
    monkeypatch.setattr(module, "SOURCE_COMPILER_PATH", ROOT / "scripts" / "compile_kb_source_packet.py")

    bundle = module.compile_source_bundle(SimpleNamespace(artifact=artifact, text=text, manifest=manifest_path))
    child, validated_manifest = module._validated_source_bundle(bundle)

    assert child["status"] == "pending_review"
    assert child["proposal_type"] == "approve_claim"
    assert validated_manifest["artifact_sha256"] == artifact_sha256


@pytest.mark.parametrize(
    ("failure_kind", "expected_error"),
    [
        ("timeout", "source compiler timed out; private diagnostic detail redacted"),
        ("launch", "source compiler could not start; private diagnostic detail redacted"),
        ("crash", "source compiler failed; private diagnostic detail redacted"),
    ],
)
def test_source_compiler_wrapper_redacts_commands_timeouts_and_child_output(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
    failure_kind: str,
    expected_error: str,
) -> None:
    module = load_module()
    sentinel = "COMPILER-WRAPPER-PRIVATE-CANARY"
    compiler = tmp_path / "compiler.py"
    compiler.write_text("# fixture\n", encoding="utf-8")
    args = SimpleNamespace(
        artifact=tmp_path / sentinel / "artifact.pdf",
        text=tmp_path / sentinel / "extracted.txt",
        manifest=tmp_path / sentinel / "manifest.json",
    )
    monkeypatch.setattr(module, "SOURCE_COMPILER_PATH", compiler)

    def fail(command, **_kwargs):
        if failure_kind == "timeout":
            raise subprocess.TimeoutExpired(command, 120, output=sentinel, stderr=sentinel)
        if failure_kind == "launch":
            raise OSError(f"could not launch {sentinel}")
        return subprocess.CompletedProcess(command, 17, stdout="", stderr=sentinel)

    monkeypatch.setattr(module.subprocess, "run", fail)

    with pytest.raises(SystemExit) as caught:
        module.compile_source_bundle(args)

    rendered = str(caught.value)
    assert rendered == expected_error
    assert sentinel not in rendered
    assert str(args.artifact) not in rendered
    assert str(args.text) not in rendered
    assert str(args.manifest) not in rendered


def test_propose_source_refuses_nonlocal_database_route(tmp_path: Path) -> None:
    module = load_module()
    args = SimpleNamespace(
        local=False,
        artifact=tmp_path / "source.pdf",
        text=tmp_path / "source.txt",
        manifest=tmp_path / "source-manifest.json",
        receipt=tmp_path / "receipt.json",
    )

    with pytest.raises(SystemExit, match="VPS-local only"):
        module.propose_source(args)


def test_source_stage_sql_allows_idempotent_readback_after_review_state_advances() -> None:
    module = load_module()
    child, _manifest = module._validated_source_bundle(source_bundle(module))

    sql = module.build_source_stage_sql(child)

    assert "on conflict (id) do nothing" in sql
    exact_clause = sql.split("select count(*) into exact_count", 1)[1].split("if marker_count", 1)[0]
    assert "status = 'pending_review'" not in exact_clause
    assert "reviewed_at is null" not in exact_clause
    assert "applied_at is null" not in exact_clause
