from __future__ import annotations

from datetime import datetime, timezone
from uuid import UUID

import pytest

from icsd.adapters import TransparencyLogReceipt, TransparencyVerification
from icsd.core.errors import (
    InvariantViolationError,
    PolicyProfileNotFoundError,
    PolicyVersionMismatchError,
    TransitionContextError,
)
from icsd.core.evidence import AuthoritySource
from icsd.core.invariants import Invariant, InvariantProfile, InvariantProfileRegistry, InvariantSet
from icsd.core.transition import Transition, TransitionContext, TransitionResult


def _balance_invariants() -> InvariantSet:
    return InvariantSet(
        [Invariant(name="non_negative_balance", check=lambda state: state["balance"] >= 0)]
    )


def _transition(mutate) -> Transition:
    return Transition(name="debit_account", mutate=mutate, invariants=_balance_invariants())


def _profile_registry() -> InvariantProfileRegistry:
    return InvariantProfileRegistry(
        [
            InvariantProfile(
                name="retail-banking",
                policy="policy/account-debit",
                policy_version="2026.03",
                invariants=_balance_invariants(),
            )
        ]
    )


class _InMemoryTransparencyLog:
    def __init__(self, *, verified: bool = True) -> None:
        self.append_calls: list[dict[str, object]] = []
        self.verify_calls: list[tuple[dict[str, object], TransparencyLogReceipt | None]] = []
        self._verified = verified

    def append(self, *, evidence_payload: dict[str, object]) -> dict[str, object]:
        self.append_calls.append(dict(evidence_payload))
        return {
            "adapter": "memory-log",
            "reference": f"entry/{len(self.append_calls)}",
            "details": {"tree_size": len(self.append_calls)},
        }

    def verify(
        self,
        *,
        evidence_payload: dict[str, object],
        receipt: TransparencyLogReceipt | None = None,
    ) -> TransparencyVerification:
        self.verify_calls.append((dict(evidence_payload), receipt))
        if self._verified:
            return TransparencyVerification(
                verified=True,
                checkpoint="checkpoint-1",
            )
        return TransparencyVerification(
            verified=False,
            reason="inclusion proof unavailable",
        )


def test_transition_apply_returns_state_snapshots_and_evidence() -> None:
    def debit(state: dict[str, int]) -> None:
        state["balance"] -= 20

    transition = _transition(debit)
    initial_state = {"balance": 100}
    result = transition.apply(
        state=initial_state,
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        authority="policy/account-debit",
        transition_id="11111111-1111-4111-8111-111111111111",
        timestamp=datetime(2026, 3, 3, 10, 0, tzinfo=timezone.utc),
    )

    assert isinstance(result, TransitionResult)
    assert result.transition_name == "debit_account"
    assert result.before_state == {"balance": 100}
    assert result.after_state == {"balance": 80}
    assert initial_state == {"balance": 100}
    assert result.evidence.timestamp == "2026-03-03T10:00:00Z"
    assert result.evidence.verify_integrity(
        before_state=result.before_state,
        after_state=result.after_state,
    )


def test_transition_apply_rejects_invalid_pre_state_without_running_mutator() -> None:
    mutate_calls = 0

    def debit(state: dict[str, int]) -> None:
        nonlocal mutate_calls
        mutate_calls += 1
        state["balance"] -= 20

    transition = _transition(debit)

    with pytest.raises(InvariantViolationError):
        transition.apply(
            state={"balance": -1},
            entity_type="account",
            entity_id="acct-001",
            actor="user:ops",
            authority="policy/account-debit",
        )

    assert mutate_calls == 0


def test_transition_apply_rejects_invalid_post_state() -> None:
    def overdraft(state: dict[str, int]) -> None:
        state["balance"] -= 120

    transition = _transition(overdraft)

    with pytest.raises(InvariantViolationError) as exc_info:
        transition.apply(
            state={"balance": 100},
            entity_type="account",
            entity_id="acct-001",
            actor="user:ops",
            authority="policy/account-debit",
        )

    assert exc_info.value.failures[0].invariant == "non_negative_balance"


def test_transition_apply_accepts_mutator_mapping_return() -> None:
    def debit(state: dict[str, int]) -> dict[str, int]:
        return {"balance": state["balance"] - 20}

    transition = _transition(debit)
    result = transition.apply(
        state={"balance": 100},
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        authority="policy/account-debit",
    )

    assert result.after_state == {"balance": 80}


def test_transition_apply_rejects_non_mapping_mutator_return() -> None:
    def invalid_mutator(_: dict[str, int]) -> int:
        return 123

    transition = _transition(invalid_mutator)

    with pytest.raises(TypeError, match="mutate return must be a mapping"):
        transition.apply(
            state={"balance": 100},
            entity_type="account",
            entity_id="acct-001",
            actor="user:ops",
            authority="policy/account-debit",
        )


def test_transition_apply_generates_transition_id_when_not_supplied() -> None:
    def noop(state: dict[str, int]) -> dict[str, int]:
        return state

    transition = _transition(noop)
    result = transition.apply(
        state={"balance": 100},
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        authority="policy/account-debit",
    )

    assert UUID(result.evidence.transition_id)


def test_transition_rejects_empty_name() -> None:
    with pytest.raises(ValueError, match="non-empty string"):
        Transition(name="", mutate=lambda _: None, invariants=_balance_invariants())


def test_transition_simple_constructs_direct_invariant_transition() -> None:
    transition = Transition.simple(
        name="debit_account",
        mutate=lambda state: state.update({"balance": state["balance"] - 20}),
        invariants=_balance_invariants(),
    )

    result = transition.apply(
        state={"balance": 100},
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        authority="policy/account-debit",
    )

    assert not transition.supports_policy_profiles
    assert result.after_state == {"balance": 80}


def test_transition_simple_requires_invariants() -> None:
    with pytest.raises(ValueError, match="requires invariants"):
        Transition.simple(
            name="debit_account",
            mutate=lambda state: state,
            invariants=None,  # type: ignore[arg-type]
        )


def test_transition_simple_preserves_authority_validator_behavior() -> None:
    transition = Transition.simple(
        name="debit_account",
        mutate=lambda state: state,
        invariants=_balance_invariants(),
        authority_validator=Transition.require_authority_sources(minimum=1),
    )

    with pytest.raises(ValueError, match="expected at least 1 authority source"):
        transition.apply(
            state={"balance": 100},
            entity_type="account",
            entity_id="acct-001",
            actor="user:ops",
            authority="policy/account-debit",
        )


def test_transition_apply_uses_profile_registry_and_derived_authority() -> None:
    def debit(state: dict[str, int]) -> None:
        state["balance"] -= 20

    transition = Transition(
        name="debit_account",
        mutate=debit,
        profile_registry=_profile_registry(),
    )
    result = transition.apply(
        state={"balance": 100},
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        profile="retail-banking",
        policy_version="2026.03",
    )

    assert result.after_state == {"balance": 80}
    assert result.evidence.authority == "policy/account-debit@2026.03"


def test_transition_apply_uses_default_profile_when_configured() -> None:
    def debit(state: dict[str, int]) -> None:
        state["balance"] -= 20

    transition = Transition(
        name="debit_account",
        mutate=debit,
        profile_registry=_profile_registry(),
        default_profile="retail-banking",
    )
    result = transition.apply(
        state={"balance": 100},
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
    )

    assert result.after_state == {"balance": 80}
    assert result.evidence.authority == "policy/account-debit@2026.03"


def test_transition_build_context_uses_default_profile_when_configured() -> None:
    transition = Transition(
        name="debit_account",
        mutate=lambda state: state,
        profile_registry=_profile_registry(),
        default_profile="retail-banking",
    )

    context = transition.build_context()

    assert context.authority == "policy/account-debit@2026.03"
    assert context.profile == "retail-banking"


def test_transition_build_context_runs_authority_validator_before_apply() -> None:
    transition = Transition(
        name="debit_account",
        mutate=lambda state: state,
        invariants=_balance_invariants(),
        authority_validator=Transition.require_authority_sources(minimum=1),
    )

    with pytest.raises(ValueError, match="expected at least 1 authority source"):
        transition.build_context(authority="policy/account-debit")


def test_transition_apply_with_context_reuses_prevalidated_context() -> None:
    transition = Transition(
        name="debit_account",
        mutate=lambda state: state.update({"balance": state["balance"] - 20}),
        invariants=_balance_invariants(),
        authority_validator=Transition.require_authority_sources(minimum=1),
    )
    context = transition.build_context(
        authority="policy/account-debit",
        authority_sources=[
            AuthoritySource(
                source_type="registry",
                source_id="uk-companies-house",
                reference="company/00000006",
            )
        ],
    )

    result = transition.apply_with_context(
        state={"balance": 100},
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        context=context,
    )

    assert result.after_state == {"balance": 80}
    assert result.evidence.authority == "policy/account-debit"


def test_transition_apply_with_context_rejects_mismatched_context_timestamp() -> None:
    transition = _transition(lambda state: state)
    context = transition.build_context(
        authority="policy/account-debit",
        active_at="2026-03-03T10:00:00Z",
    )

    with pytest.raises(TransitionContextError, match="timestamp must match context.active_at"):
        transition.apply_with_context(
            state={"balance": 100},
            entity_type="account",
            entity_id="acct-001",
            actor="user:ops",
            context=context,
            timestamp="2026-03-03T10:00:01Z",
        )


def test_transition_apply_with_context_rejects_non_context_input() -> None:
    transition = _transition(lambda state: state)

    with pytest.raises(TransitionContextError, match="TransitionContext instance"):
        transition.apply_with_context(
            state={"balance": 100},
            entity_type="account",
            entity_id="acct-001",
            actor="user:ops",
            context="invalid",  # type: ignore[arg-type]
        )


def test_profile_transition_allows_explicit_authority_override() -> None:
    transition = Transition(
        name="debit_account",
        mutate=lambda state: state,
        profile_registry=_profile_registry(),
    )

    result = transition.apply(
        state={"balance": 100},
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        profile="retail-banking",
        policy_version="2026.03",
        authority="policy/manual-override",
    )

    assert result.evidence.authority == "policy/manual-override"


def test_transition_rejects_profile_inputs_when_not_profile_based() -> None:
    transition = _transition(lambda state: state)

    with pytest.raises(ValueError, match="profile requires profile_registry"):
        transition.apply(
            state={"balance": 100},
            entity_type="account",
            entity_id="acct-001",
            actor="user:ops",
            authority="policy/account-debit",
            profile="retail-banking",
        )


def test_transition_context_rejects_empty_authority() -> None:
    with pytest.raises(TransitionContextError, match="authority must be a non-empty string"):
        TransitionContext(authority="")


def test_transition_apply_rejects_unknown_profile() -> None:
    transition = Transition(
        name="debit_account",
        mutate=lambda state: state,
        profile_registry=_profile_registry(),
    )

    with pytest.raises(PolicyProfileNotFoundError, match="Unknown invariant profile"):
        transition.apply(
            state={"balance": 100},
            entity_type="account",
            entity_id="acct-001",
            actor="user:ops",
            profile="missing-profile",
        )


def test_transition_apply_rejects_policy_version_mismatch() -> None:
    transition = Transition(
        name="debit_account",
        mutate=lambda state: state,
        profile_registry=_profile_registry(),
    )

    with pytest.raises(PolicyVersionMismatchError, match="Policy version mismatch"):
        transition.apply(
            state={"balance": 100},
            entity_type="account",
            entity_id="acct-001",
            actor="user:ops",
            profile="retail-banking",
            policy_version="2026.02",
        )


def test_transition_apply_requires_profile_without_default_profile() -> None:
    transition = Transition(
        name="debit_account",
        mutate=lambda state: state,
        profile_registry=_profile_registry(),
    )

    with pytest.raises(ValueError, match="profile must be provided"):
        transition.apply(
            state={"balance": 100},
            entity_type="account",
            entity_id="acct-001",
            actor="user:ops",
        )


def test_transition_apply_includes_authority_sources_in_evidence() -> None:
    transition = _transition(lambda state: state)
    result = transition.apply(
        state={"balance": 100},
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        authority="policy/account-debit",
        authority_sources=[
            {
                "source_type": "registry",
                "source_id": "uk-companies-house",
                "reference": "company/00000006",
            }
        ],
    )

    assert len(result.evidence.authority_sources) == 1
    assert result.evidence.authority_sources[0].source_id == "uk-companies-house"


def test_transition_apply_runs_authority_validator_hook() -> None:
    captured: dict[str, str | int] = {}

    def validator(authority: str, sources: tuple[AuthoritySource, ...]) -> None:
        captured["authority"] = authority
        captured["source_count"] = len(sources)

    transition = Transition(
        name="debit_account",
        mutate=lambda state: state,
        profile_registry=_profile_registry(),
        authority_validator=validator,
    )
    transition.apply(
        state={"balance": 100},
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        profile="retail-banking",
        policy_version="2026.03",
        authority_sources=[
            AuthoritySource(
                source_type="policy",
                source_id="hmrc-guidance",
                reference="hmrc-policy/v2026.03",
            )
        ],
    )

    assert captured == {
        "authority": "policy/account-debit@2026.03",
        "source_count": 1,
    }


def test_transition_require_authority_sources_enforces_minimum() -> None:
    transition = Transition(
        name="debit_account",
        mutate=lambda state: state,
        invariants=_balance_invariants(),
        authority_validator=Transition.require_authority_sources(minimum=1),
    )

    with pytest.raises(ValueError, match="expected at least 1 authority source"):
        transition.apply(
            state={"balance": 100},
            entity_type="account",
            entity_id="acct-001",
            actor="user:ops",
            authority="policy/account-debit",
        )


def test_transition_apply_appends_to_transparency_log_and_records_provenance() -> None:
    transparency_log = _InMemoryTransparencyLog()
    transition = Transition(
        name="debit_account",
        mutate=lambda state: state,
        invariants=_balance_invariants(),
        transparency_log=transparency_log,
    )

    result = transition.apply(
        state={"balance": 100},
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        authority="policy/account-debit",
    )

    assert len(transparency_log.append_calls) == 1
    assert result.evidence.provenance[0].source == "transparency-log/memory-log"
    assert result.evidence.provenance[0].reference == "entry/1"


def test_transition_verify_transparency_uses_adapter_and_receipt() -> None:
    transparency_log = _InMemoryTransparencyLog()
    transition = Transition(
        name="debit_account",
        mutate=lambda state: state,
        invariants=_balance_invariants(),
        transparency_log=transparency_log,
    )
    result = transition.apply(
        state={"balance": 100},
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        authority="policy/account-debit",
    )

    verification = transition.verify_transparency(result.evidence)

    assert verification.verified
    assert len(transparency_log.verify_calls) == 1
    _, receipt = transparency_log.verify_calls[0]
    assert receipt is not None
    assert receipt.reference == "entry/1"


def test_transition_apply_rejects_failed_transparency_verification_when_enforced() -> None:
    transition = Transition(
        name="debit_account",
        mutate=lambda state: state,
        invariants=_balance_invariants(),
        transparency_log=_InMemoryTransparencyLog(verified=False),
        verify_transparency_after_append=True,
    )

    with pytest.raises(ValueError, match="Transparency log verification failed"):
        transition.apply(
            state={"balance": 100},
            entity_type="account",
            entity_id="acct-001",
            actor="user:ops",
            authority="policy/account-debit",
        )


def test_transition_verify_transparency_requires_adapter() -> None:
    transition = _transition(lambda state: state)

    with pytest.raises(ValueError, match="no transparency_log adapter configured"):
        transition.verify_transparency(
            {
                "schema_version": "0.1",
                "transition_id": "11111111-1111-4111-8111-111111111111",
                "entity_type": "account",
                "entity_id": "acct-001",
                "actor": "user:ops",
                "authority": "policy/account-debit",
                "timestamp": "2026-03-03T10:00:00Z",
                "before_hash": "0" * 64,
                "after_hash": "1" * 64,
            }
        )


def test_transition_rejects_verify_transparency_without_adapter() -> None:
    with pytest.raises(ValueError, match="requires transparency_log"):
        Transition(
            name="debit_account",
            mutate=lambda state: state,
            invariants=_balance_invariants(),
            verify_transparency_after_append=True,
        )
