from __future__ import annotations

import json
import pathlib
import tempfile
import unittest
from datetime import datetime, timedelta, timezone

from reliability_core.validation_control import (
    CheckpointStore,
    ValidationStateError,
    readiness_digest,
    receipt_digest,
)
from reliability_core.validation_tiering import EXPECTED_POLICY_DIGEST


ROOT = pathlib.Path(__file__).resolve().parents[1]
POLICY = json.loads((ROOT / "validation" / "validation_policy.json").read_text(encoding="utf-8"))
BINDINGS = {
    "source_tree_sha256": "1" * 64,
    "candidate_sha256": "2" * 64,
    "runner_sha256": "3" * 64,
    "orchestrator_sha256": "4" * 64,
    "authority_sha256": "5" * 64,
}


def change_for(role: str = "product", *, minimum_tier: str = "T0") -> dict[str, object]:
    return {
        "changed_roles": [role],
        "risk_signals": ["resume"] if role == "orchestrator" else [],
        "modules": ["validation-control"] if role not in {"docs", "manifest", "packaging"} else [],
        "flags": {"orchestrator_change": role == "orchestrator"},
        "dependency_known": True,
        "global_scope": False,
        "policy_digest": EXPECTED_POLICY_DIGEST,
        "classifier_version": "2.0.0",
        "minimum_tier": minimum_tier,
    }


def readiness(*, expired: bool = False, marker: str = "a") -> dict[str, object]:
    now = datetime.now(timezone.utc)
    checked = now - timedelta(minutes=10 if expired else 1)
    valid = now - timedelta(minutes=1) if expired else now + timedelta(minutes=5)
    value: dict[str, object] = {
        "status": "PASS",
        "checked_at": checked.isoformat(),
        "valid_until": valid.isoformat(),
        "environment_fingerprint": marker * 64,
        "cleanup_state": "PASS",
        "task_owned_container_count": 0,
        "orphan_process_count": 0,
        "zombie_process_count": 0,
    }
    value["readiness_sha256"] = readiness_digest(value)
    return value


class ValidationControlTests(unittest.TestCase):
    def setUp(self) -> None:
        self.temp = tempfile.TemporaryDirectory()
        self.addCleanup(self.temp.cleanup)
        self.state = pathlib.Path(self.temp.name) / "state"
        self.change = change_for()
        self.store = CheckpointStore.initialize(
            self.state,
            policy=POLICY,
            change=self.change,
            bindings=BINDINGS,
            tasks=["targeted", "cleanup"],
        )

    def open_session(self, *, session_id: str = "session-1", marker: str = "a") -> dict[str, object]:
        return self.store.open_session(readiness(marker=marker), session_id=session_id)

    def make_receipt(
        self,
        session: dict[str, object],
        *,
        status: str = "PASS",
        ordinal: int = 1,
        task_id: str | None = None,
    ) -> dict[str, object]:
        state = self.store.inspect()
        session_readiness = session["readiness"]
        assert isinstance(session_readiness, dict)
        value: dict[str, object] = {
            "task_id": task_id or state["next_task"],
            "status": status,
            "complete": True,
            "immutable": True,
            "cleanup_state": "PASS",
            "run_id": f"run-{ordinal}",
            "challenge_nonce": f"challenge-{ordinal}",
            "process_identity_nonce": f"process-{ordinal}",
            "session_id": session["session_id"],
            "readiness_sha256": session_readiness["readiness_sha256"],
            "bindings": BINDINGS,
            "predecessor_receipt_sha256": state["predecessor_receipt_sha256"],
            "evidence_sha256": format(ordinal, "064x"),
        }
        value["receipt_sha256"] = receipt_digest(value)
        return value

    def test_initial_plan_is_tiered_and_resumable(self) -> None:
        state = self.store.inspect()
        self.assertEqual("T1", state["tier"])
        self.assertEqual("targeted", state["next_task"])
        decision = self.store.resume_decision(
            policy=POLICY, change=self.change, bindings=BINDINGS
        )
        self.assertEqual("RESUME", decision["decision"])

    def test_pass_fail_retry_and_complete(self) -> None:
        first_session = self.open_session()
        self.store.record_result(self.make_receipt(first_session, ordinal=1))
        failed = self.make_receipt(first_session, status="FAIL", ordinal=2)
        self.store.record_result(failed)
        self.assertEqual("cleanup", self.store.inspect()["next_task"])
        second_session = self.open_session(session_id="session-2", marker="b")
        self.store.record_result(self.make_receipt(second_session, ordinal=3))
        state = self.store.inspect()
        self.assertTrue(state["complete"])
        self.assertEqual(2, state["completed_task_count"])
        self.assertEqual(3, state["attempt_count"])
        self.assertEqual(
            "COMPLETE",
            self.store.resume_decision(
                policy=POLICY, change=self.change, bindings=BINDINGS
            )["decision"],
        )

    def test_interruption_reopens_at_next_unfinished_task(self) -> None:
        session = self.open_session()
        self.store.record_result(self.make_receipt(session, ordinal=1))
        reopened = CheckpointStore(self.state)
        decision = reopened.resume_decision(
            policy=POLICY, change=self.change, bindings=BINDINGS
        )
        self.assertEqual("RESUME", decision["decision"])
        self.assertEqual("cleanup", decision["next_task"])

    def test_duplicate_identity_is_rejected_before_append(self) -> None:
        session = self.open_session()
        self.store.record_result(self.make_receipt(session, ordinal=1))
        duplicate = self.make_receipt(session, ordinal=2)
        duplicate["run_id"] = "run-1"
        duplicate["receipt_sha256"] = receipt_digest(duplicate)
        before = self.store.inspect()["event_count"]
        with self.assertRaisesRegex(ValidationStateError, "receipt_identity_reused:run_id"):
            self.store.record_result(duplicate)
        self.assertEqual(before, self.store.inspect()["event_count"])

    def test_bad_receipt_digest_is_rejected_before_append(self) -> None:
        session = self.open_session()
        bad = self.make_receipt(session, ordinal=1)
        bad["receipt_sha256"] = "0" * 64
        before = self.store.inspect()["event_count"]
        with self.assertRaisesRegex(ValidationStateError, "receipt_digest_mismatch"):
            self.store.record_result(bad)
        self.assertEqual(before, self.store.inspect()["event_count"])

    def test_out_of_order_result_is_rejected(self) -> None:
        session = self.open_session()
        bad = self.make_receipt(session, ordinal=1, task_id="cleanup")
        with self.assertRaisesRegex(ValidationStateError, "receipt_out_of_order"):
            self.store.record_result(bad)

    def test_expired_readiness_is_rejected_without_poisoning_state(self) -> None:
        with self.assertRaisesRegex(ValidationStateError, "readiness_expired"):
            self.store.open_session(readiness(expired=True), session_id="expired")
        self.assertEqual(0, self.store.inspect()["event_count"])

    def test_binding_change_blocks_resume(self) -> None:
        changed = dict(BINDINGS)
        changed["source_tree_sha256"] = "f" * 64
        decision = self.store.resume_decision(
            policy=POLICY, change=self.change, bindings=changed
        )
        self.assertEqual("BLOCKED", decision["decision"])
        self.assertEqual("immutable_binding_mismatch", decision["reason"])

    def test_tier_upgrade_requires_new_plan(self) -> None:
        decision = self.store.resume_decision(
            policy=POLICY, change=change_for("orchestrator"), bindings=BINDINGS
        )
        self.assertEqual("BLOCKED", decision["decision"])
        self.assertEqual("tier_upgrade_requires_new_plan", decision["reason"])
        self.assertEqual("T4", decision["required_tier"])

    def test_tier_downgrade_cannot_redefine_existing_plan(self) -> None:
        terminal_state = pathlib.Path(self.temp.name) / "terminal"
        terminal_change = change_for("orchestrator")
        terminal = CheckpointStore.initialize(
            terminal_state,
            policy=POLICY,
            change=terminal_change,
            bindings=BINDINGS,
            tasks=["terminal"],
        )
        decision = terminal.resume_decision(
            policy=POLICY, change=change_for("docs"), bindings=BINDINGS
        )
        self.assertEqual("BLOCKED", decision["decision"])
        self.assertEqual("change_definition_mismatch_no_downgrade", decision["reason"])
        self.assertEqual("T4", decision["effective_tier"])

    def test_event_tamper_breaks_the_chain(self) -> None:
        self.open_session()
        path = self.state / "events" / "00000000.json"
        event = json.loads(path.read_text(encoding="utf-8"))
        event["session_id"] = "tampered"
        path.write_text(json.dumps(event), encoding="utf-8")
        with self.assertRaisesRegex(ValidationStateError, "event_digest_mismatch"):
            self.store.inspect()

    def test_plan_tamper_is_fail_closed(self) -> None:
        plan_path = self.state / "plan.json"
        plan = json.loads(plan_path.read_text(encoding="utf-8"))
        plan["tier"] = "T0"
        plan_path.write_text(json.dumps(plan), encoding="utf-8")
        with self.assertRaisesRegex(ValidationStateError, "plan_digest_mismatch"):
            self.store.inspect()


if __name__ == "__main__":
    unittest.main()
