from __future__ import annotations

import hashlib
import importlib.util
import json
import os
import shutil
from datetime import datetime
from pathlib import Path
from typing import Any

import pytest

HERE = Path(__file__).resolve().parent
SPEC = importlib.util.spec_from_file_location("post_claim_adjudicator", HERE / "post_claim_adjudicator.py")
assert SPEC is not None and SPEC.loader is not None
adjudicator = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(adjudicator)
LIVE = Path("/home/cube/.hermes/profiles/dualcoachtest")
EVIDENCE = HERE.parent


def tree_hash(root: Path) -> str:
    digest = hashlib.sha256()
    for path in sorted(item for item in root.rglob("*") if item.is_file()):
        digest.update(str(path.relative_to(root)).encode())
        digest.update(path.read_bytes())
    return digest.hexdigest()


def fixture(tmp_path: Path, *, registry_rows: bool = False) -> Path:
    profile = tmp_path / "profile"
    ledger_source = LIVE / "data/onboarding/telegram-customer-bootstrap-v1/ledger.json"
    ledger = profile / "data/onboarding/telegram-customer-bootstrap-v1/ledger.json"
    ledger.parent.mkdir(parents=True, mode=0o700)
    shutil.copyfile(ledger_source, ledger)
    ledger.chmod(0o600)
    value = json.loads(ledger.read_text())
    updated = datetime.fromisoformat(value["sessions"][1]["updated_at"]).timestamp()
    os.utime(ledger, (updated, updated))
    registry = profile / "customers/registry.json"
    registry.parent.mkdir(mode=0o700)
    live_registry = json.loads((LIVE / "customers/registry.json").read_text())
    if not registry_rows:
        live_registry["customers"] = []
    registry.write_text(json.dumps(live_registry, sort_keys=True, separators=(",", ":")))
    registry.chmod(0o600)
    return profile


def service_stub() -> dict[str, Any]:
    return {"service": "active/running", "candidate_modules": {}, "normal_path_transitions": []}


def canonical_ledger(path: Path, value: dict[str, Any]) -> None:
    payload = {"schema": value["schema"], "sessions": value["sessions"]}
    value["digest"] = hashlib.sha256(adjudicator.canonical(payload)).hexdigest()
    path.write_bytes(adjudicator.canonical(value))
    path.chmod(0o600)
    updated = datetime.fromisoformat(value["sessions"][1]["updated_at"]).timestamp()
    os.utime(path, (updated, updated))


def test_exact_post_claim_predicate_passes_without_registry_row(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    profile = fixture(tmp_path)
    monkeypatch.setattr(adjudicator, "source_semantics_and_service", service_stub)
    before = tree_hash(profile)
    receipt = adjudicator.adjudicate(profile, EVIDENCE)
    assert receipt["status"] == "PASS_SUCCESSFUL_CLAIM"
    assert receipt["accepted_role_claim_count"] == 1
    assert receipt["consent_publication_count"] == 1
    assert receipt["preparation_ledger_sha256"] == adjudicator.PREPARATION_LEDGER_SHA256
    assert receipt["current_payload_digest"] == adjudicator.CURRENT_LEDGER_DIGEST
    assert receipt["token_binding"]["raw_token_only_private_handoff"] is True
    assert tree_hash(profile) == before
    assert "start_token" not in json.dumps(receipt)


def test_live_normal_path_registry_row_fails_requested_absence(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    profile = fixture(tmp_path, registry_rows=True)
    monkeypatch.setattr(adjudicator, "source_semantics_and_service", service_stub)
    with pytest.raises(adjudicator.AdjudicationError, match="required no customer registry row, observed 1"):
        adjudicator.adjudicate(profile, EVIDENCE)


@pytest.mark.parametrize(
    ("attack", "message"),
    [
        ("prior", "old EXPIRED session changed"),
        ("claim", "accepted role claim"),
        ("draft", "customer draft changed"),
        ("card", "replacement final state"),
        ("digest", "bootstrap ledger digest"),
    ],
)
def test_unknown_or_mismatched_authority_fails_closed(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch, attack: str, message: str
) -> None:
    profile = fixture(tmp_path)
    monkeypatch.setattr(adjudicator, "source_semantics_and_service", service_stub)
    path = profile / "data/onboarding/telegram-customer-bootstrap-v1/ledger.json"
    value = json.loads(path.read_text())
    if attack == "prior":
        value["sessions"][0]["generation"] = 3
    elif attack == "claim":
        value["sessions"][1]["role_claims"].append(dict(value["sessions"][1]["role_claims"][0]))
    elif attack == "draft":
        value["sessions"][1]["customer_draft"]["calories_kcal"] = 2100
    elif attack == "card":
        value["sessions"][1]["consent_card_message_id"] = "160"
    else:
        value["digest"] = "0" * 64
    if attack != "digest":
        with pytest.raises(adjudicator.AdjudicationError, match=message):
            adjudicator.exact_sessions(value)
    else:
        path.write_bytes(adjudicator.canonical(value))
        with pytest.raises(adjudicator.AdjudicationError, match=message):
            adjudicator.validate_ledger(profile)


def test_activation_or_delivery_authority_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    profile = fixture(tmp_path)
    monkeypatch.setattr(adjudicator, "source_semantics_and_service", service_stub)
    forbidden = profile / "data/scheduled-deliveries.jsonl"
    forbidden.parent.mkdir(exist_ok=True)
    forbidden.write_text("")
    with pytest.raises(adjudicator.AdjudicationError, match="activation/checkin/generation/delivery"):
        adjudicator.adjudicate(profile, EVIDENCE)


def test_receipt_is_append_only_one_shot(tmp_path: Path) -> None:
    receipt = tmp_path / "receipt.json"
    adjudicator.exclusive_receipt(receipt, {"status": "FAIL"})
    with pytest.raises(FileExistsError):
        adjudicator.exclusive_receipt(receipt, {"status": "PASS_SUCCESSFUL_CLAIM"})
    assert json.loads(receipt.read_text()) == {"status": "FAIL"}
