from __future__ import annotations

import argparse
import copy
import hashlib
import json
import os
import stat
import sys
from pathlib import Path

import pytest

from ops import run_local_genesis_ledger_rebuild as genesis_rebuild

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

import export_kb_transition_replay_material as exporter  # noqa: E402
import kb_apply_replay_receipt as replay_receipt  # noqa: E402

PROPOSAL_ID = "70000000-0000-4000-8000-000000000001"
REVIEWER_ID = "70000000-0000-4000-8000-000000000002"
SERVICE_ID = "70000000-0000-4000-8000-000000000003"
CLAIM_A = "70000000-0000-4000-8000-000000000004"
CLAIM_B = "70000000-0000-4000-8000-000000000005"


def _material() -> dict:
    payload = {
        "apply_payload": {
            "from_claim": CLAIM_A,
            "to_claim": CLAIM_B,
            "edge_type": "supports",
            "weight": 0.75,
        },
        "title": "Private exact transition packet",
    }
    approved = {
        "id": PROPOSAL_ID,
        "proposal_type": "add_edge",
        "status": "approved",
        "proposed_by_handle": "leo",
        "proposed_by_agent_id": REVIEWER_ID,
        "channel": "private_export_test",
        "source_ref": "private://transition/source",
        "rationale": "Retain the exact approved row for deterministic reconstruction.",
        "payload": payload,
        "reviewed_by_handle": "m3ta",
        "reviewed_by_agent_id": REVIEWER_ID,
        "reviewed_at": "2026-07-18T10:01:00+00:00",
        "review_note": "Approved the exact strict packet.",
        "applied_by_handle": None,
        "applied_by_agent_id": None,
        "applied_at": None,
        "created_at": "2026-07-18T10:00:00+00:00",
        "updated_at": "2026-07-18T10:01:00.000001+00:00",
    }
    applied = {
        **approved,
        "status": "applied",
        "applied_by_handle": "kb-apply",
        "applied_by_agent_id": SERVICE_ID,
        "applied_at": "2026-07-18T10:02:00+00:00",
        "updated_at": "2026-07-18T10:02:00.000001+00:00",
    }
    approval = {
        "proposal_id": PROPOSAL_ID,
        "proposal_type": "add_edge",
        "payload": payload,
        "reviewed_by_handle": "m3ta",
        "reviewed_by_agent_id": REVIEWER_ID,
        "reviewed_by_db_role": "kb_review",
        "reviewed_at": "2026-07-18T10:01:00+00:00",
        "review_note": "Approved the exact strict packet.",
    }
    receipt_proposal = {field: applied[field] for field in exporter.RECEIPT_PROPOSAL_FIELDS}
    receipt = replay_receipt.build_replay_receipt(
        receipt_proposal,
        {
            "claim_edges": [
                {
                    "id": "70000000-0000-4000-8000-000000000006",
                    "from_claim": CLAIM_A,
                    "to_claim": CLAIM_B,
                    "edge_type": "supports",
                    "weight": 0.75,
                    "created_by": None,
                    "created_at": "2026-07-18T10:01:30+00:00",
                }
            ]
        },
        apply_sql_sha256=replay_receipt.sha256_text("exact guarded apply SQL"),
        apply_sql_source="exact_executed_sql",
        exported_at_utc="2026-07-18T10:03:00+00:00",
    )
    return {
        "artifact": exporter.MATERIAL_ARTIFACT,
        "contract_version": exporter.MATERIAL_CONTRACT_VERSION,
        "sequence": 1,
        "approved_proposal": approved,
        "approval_snapshot": approval,
        "applied_proposal": applied,
        "replay_receipt": receipt,
    }


def _rehash_worker_receipt(material: dict) -> dict:
    receipt = material["replay_receipt"]

    def sha256_json(value) -> str:
        encoded = json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True).encode("utf-8")
        return hashlib.sha256(encoded).hexdigest()

    rows = receipt["canonical_rows"]
    replay_material = {
        "receipt_contract_version": receipt["receipt_contract_version"],
        "apply_engine": receipt["apply_engine"],
        "proposal": receipt["proposal"],
        "canonical_rows": rows,
    }
    receipt["hashes"] = {
        "apply_payload_sha256": sha256_json(receipt["proposal"]["payload"]["apply_payload"]),
        "proposal_payload_sha256": sha256_json(receipt["proposal"]["payload"]),
        "canonical_rows_sha256": sha256_json(rows),
        "row_sha256": {table: [sha256_json(row) for row in table_rows] for table, table_rows in rows.items()},
        "table_sha256": {table: sha256_json(table_rows) for table, table_rows in rows.items()},
        "replay_material_sha256": sha256_json(replay_material),
    }
    return material


def _legacy_space_timestamp_material() -> dict:
    material = copy.deepcopy(_material())
    receipt = material["replay_receipt"]
    receipt["proposal"]["reviewed_at"] = "2026-07-18 10:01:00+00"
    receipt["proposal"]["applied_at"] = "2026-07-18 10:02:00+00"
    receipt["canonical_rows"]["claim_edges"][0]["created_at"] = "2026-07-18 10:01:30+00"
    receipt["exported_at_utc"] = "2026-07-18T10:03:00+00:00"
    return _rehash_worker_receipt(material)


def test_material_matches_genesis_replay_row_contract() -> None:
    material = exporter.validate_material(
        _material(),
        expected_proposal_id=PROPOSAL_ID,
        expected_sequence=1,
    )

    assert exporter.MATERIAL_ARTIFACT == genesis_rebuild.MATERIAL_ARTIFACT
    assert exporter.MATERIAL_CONTRACT_VERSION == genesis_rebuild.MATERIAL_CONTRACT_VERSION
    assert exporter.PROPOSAL_FIELDS == genesis_rebuild.PROPOSAL_FIELDS
    assert exporter.APPROVAL_FIELDS == genesis_rebuild.APPROVAL_FIELDS
    genesis_rebuild._validate_proposal_rows(
        material["approved_proposal"],
        material["applied_proposal"],
        material["approval_snapshot"],
        material["replay_receipt"]["proposal"],
    )


def test_legacy_postgres_worker_receipt_exports_and_reconstructs_without_rehashing(
    tmp_path: Path,
) -> None:
    material = _legacy_space_timestamp_material()
    legacy_hash = material["replay_receipt"]["hashes"]["replay_material_sha256"]

    validated = exporter.validate_material(
        material,
        expected_proposal_id=PROPOSAL_ID,
        expected_sequence=1,
    )
    assert validated["replay_receipt"]["proposal"]["reviewed_at"] == "2026-07-18 10:01:00+00"
    assert validated["replay_receipt"]["hashes"]["replay_material_sha256"] == legacy_hash

    material_path = tmp_path / "legacy-transition.json"
    material_path.write_text(json.dumps(validated), encoding="utf-8")
    entry = genesis_rebuild._validate_entry_material(
        validated,
        expected_sequence=1,
        expected_replay_hash=legacy_hash,
        material_path=material_path,
        material_sha256=hashlib.sha256(material_path.read_bytes()).hexdigest(),
    )
    assert entry.replay_material_sha256 == legacy_hash
    assert entry.receipt["proposal"]["reviewed_at"] == "2026-07-18T10:01:00.000000Z"
    assert entry.receipt["canonical_rows"]["claim_edges"][0]["created_at"] == ("2026-07-18T10:01:30.000000Z")

    tampered = copy.deepcopy(material)
    tampered["replay_receipt"]["hashes"]["replay_material_sha256"] = "0" * 64
    with pytest.raises(exporter.ExportError, match="worker replay receipt"):
        exporter.validate_material(tampered, expected_proposal_id=PROPOSAL_ID)


@pytest.mark.parametrize(
    "separator",
    ["X", "_", "\U0001f642"],
    ids=["x", "underscore", "emoji"],
)
def test_current_material_rejects_noncanonical_timestamp_separators(separator: str) -> None:
    material = _material()
    invalid = material["approved_proposal"]["reviewed_at"].replace("T", separator, 1)
    material["approved_proposal"]["reviewed_at"] = invalid
    material["applied_proposal"]["reviewed_at"] = invalid
    material["approval_snapshot"]["reviewed_at"] = invalid

    with pytest.raises(exporter.ExportError, match="timezone-aware timestamp"):
        exporter.validate_material(material, expected_proposal_id=PROPOSAL_ID)


@pytest.mark.parametrize(
    "separator",
    ["X", "_", "\U0001f642"],
    ids=["x", "underscore", "emoji"],
)
@pytest.mark.parametrize("target", ["proposal", "canonical_row"])
def test_rehashed_legacy_receipt_rejects_hybrid_timestamp_separators(
    separator: str,
    target: str,
) -> None:
    material = _legacy_space_timestamp_material()
    receipt = material["replay_receipt"]
    if target == "proposal":
        receipt["proposal"]["applied_at"] = receipt["proposal"]["applied_at"].replace(" ", separator, 1)
    else:
        row = receipt["canonical_rows"]["claim_edges"][0]
        row["created_at"] = row["created_at"].replace(" ", separator, 1)
    _rehash_worker_receipt(material)

    with pytest.raises(exporter.ExportError, match="worker replay receipt"):
        exporter.validate_material(material, expected_proposal_id=PROPOSAL_ID)


@pytest.mark.parametrize(
    ("mutation", "message"),
    [
        (lambda value: value["approved_proposal"].__setitem__("unexpected", True), "invalid fields"),
        (
            lambda value: value["applied_proposal"].__setitem__("rationale", "post-approval drift"),
            "exact guarded apply transition",
        ),
        (
            lambda value: value["approval_snapshot"].__setitem__("review_note", "different review"),
            "does not match approved proposal",
        ),
        (
            lambda value: value["replay_receipt"]["proposal"].__setitem__("id", CLAIM_A),
            "worker replay receipt",
        ),
        (lambda value: value.__setitem__("sequence", 0), "positive integer"),
    ],
)
def test_material_validation_fails_closed_on_drift(mutation, message: str) -> None:
    material = copy.deepcopy(_material())
    mutation(material)

    with pytest.raises(exporter.ExportError, match=message):
        exporter.validate_material(material, expected_proposal_id=PROPOSAL_ID)


def test_json_parser_rejects_duplicate_and_nonfinite_fields() -> None:
    with pytest.raises(exporter.ExportError, match="duplicate"):
        exporter._parse_json_object('{"artifact":"a","artifact":"b"}', label="fixture")
    with pytest.raises(exporter.ExportError, match="invalid numeric"):
        exporter._parse_json_object('{"sequence":NaN}', label="fixture")


def test_private_writer_is_atomic_idempotent_and_refuses_alias_or_rerun_drift(tmp_path: Path) -> None:
    material = exporter.validate_material(_material(), expected_proposal_id=PROPOSAL_ID)
    protected = tmp_path / "worker-receipt.json"
    protected.write_text("private worker receipt", encoding="utf-8")
    protected.chmod(0o600)
    output = tmp_path / "material.json"

    written, disposition = exporter.write_private_material(output, material, protected_paths=(protected,))
    assert written == output
    assert disposition == "created"
    assert stat.S_IMODE(output.stat().st_mode) == 0o600
    assert output.stat().st_nlink == 1
    assert json.loads(output.read_text(encoding="utf-8")) == material

    _, rerun_disposition = exporter.write_private_material(output, material, protected_paths=(protected,))
    assert rerun_disposition == "unchanged"

    drifted = copy.deepcopy(material)
    drifted["sequence"] = 2
    with pytest.raises(exporter.ExportError, match="different replay material"):
        exporter.write_private_material(output, drifted, protected_paths=(protected,))
    with pytest.raises(exporter.ExportError, match="aliases a protected"):
        exporter.write_private_material(protected, material, protected_paths=(protected,))

    dangling = tmp_path / "dangling-output.json"
    dangling.symlink_to(tmp_path / "missing-target.json")
    with pytest.raises(exporter.ExportError, match="non-symlink"):
        exporter.write_private_material(dangling, material, protected_paths=(protected,))

    hardlink = tmp_path / "hardlink.json"
    os.link(output, hardlink)
    with pytest.raises(exporter.ExportError, match="hard-link"):
        exporter.write_private_material(output, material, protected_paths=(protected,))


def test_private_receipt_loader_requires_exact_mode_and_rejects_symlinks(tmp_path: Path) -> None:
    receipt = _material()["replay_receipt"]
    receipt_path = tmp_path / "receipt.json"
    receipt_path.write_text(json.dumps(receipt), encoding="utf-8")
    receipt_path.chmod(0o600)
    assert exporter._load_private_receipt(receipt_path) == receipt

    receipt_path.chmod(0o640)
    with pytest.raises(exporter.ExportError, match="exactly 0600"):
        exporter._load_private_receipt(receipt_path)

    receipt_path.chmod(0o600)
    receipt_link = tmp_path / "receipt-link.json"
    receipt_link.symlink_to(receipt_path)
    with pytest.raises(exporter.ExportError, match="non-symlink"):
        exporter._load_private_receipt(receipt_link)


def test_transition_fetch_uses_only_read_only_narrow_function(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[str, bool]] = []
    rows = {key: _material()[key] for key in ("approved_proposal", "approval_snapshot", "applied_proposal")}

    def fake_run_psql(args, sql: str, password: str, *, redact_output_on_error: bool = False) -> str:
        assert args.role == "kb_apply"
        assert password == "private-password"
        calls.append((sql, redact_output_on_error))
        return json.dumps(rows)

    monkeypatch.setattr(exporter.apply_engine, "run_psql", fake_run_psql)
    args = argparse.Namespace(container="disposable", db="teleo", host="127.0.0.1", role="kb_apply")

    assert exporter.fetch_transition_rows(args, "private-password", PROPOSAL_ID) == rows
    assert len(calls) == 1
    sql, redacted = calls[0]
    assert "transaction isolation level repeatable read read only" in sql
    assert "kb_stage.export_applied_proposal_replay_rows" in sql
    assert not any(keyword in sql.lower() for keyword in ("insert ", "update ", "delete ", "truncate "))
    assert redacted is True
