from __future__ import annotations

from copy import deepcopy

import pytest

from icsd.core.errors import InvariantViolationError
from icsd.core.invariants import Invariant, InvariantSet
from icsd.core.serialise import canonical_json_dumps, hash_canonical_json
from icsd.core.transition import Transition


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


class _LyingAuthorityAdapter:
    def __init__(self) -> None:
        self.calls: list[tuple[dict[str, object], dict[str, object]]] = []
        self.last_proposal: dict[str, object] | None = None

    def propose(
        self,
        *,
        current_state: dict[str, object],
        authority_response: dict[str, object],
    ) -> dict[str, object]:
        self.calls.append((dict(current_state), dict(authority_response)))
        proposal = {
            "changes": {
                "balance": authority_response["approved_balance"],
            },
            "source": "authority-api/mock-ledger",
            "reference": authority_response["response_id"],
            "details": {
                "provider": "mock-authority",
                "claim": "externally approved overdraft",
            },
        }
        self.last_proposal = deepcopy(proposal)
        return proposal


def _build_refusal_report(
    *,
    transition_name: str,
    entity_type: str,
    entity_id: str,
    actor: str,
    authority: str,
    authority_response: dict[str, object],
    proposal: dict[str, object],
    error: InvariantViolationError,
    refused_at: str,
) -> dict[str, object]:
    base_report: dict[str, object] = {
        "schema_version": "refusal-0.1",
        "status": "refused",
        "transition": transition_name,
        "entity_type": entity_type,
        "entity_id": entity_id,
        "actor": actor,
        "authority": authority,
        "refused_at": refused_at,
        "authority_response": deepcopy(authority_response),
        "proposal": deepcopy(proposal),
        "reason": error.as_dict(),
    }
    return {
        **base_report,
        "refusal_id": hash_canonical_json(base_report),
    }


def test_trustless_authority_refusal_report_is_deterministic() -> None:
    mutate_calls = 0
    adapter = _LyingAuthorityAdapter()

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

    transition = Transition(
        name="debit_account",
        mutate=mutate,
        invariants=_balance_invariants(),
        authority_adapter=adapter,
    )
    current_state = {"balance": 100, "currency": "GBP"}
    authority_response = {
        "approved_balance": -999,
        "response_id": "resp-009",
    }

    with pytest.raises(InvariantViolationError) as exc_info:
        transition.apply(
            state=current_state,
            entity_type="account",
            entity_id="acct-099",
            actor="user:ops",
            authority="policy/account-debit",
            authority_response=authority_response,
            timestamp="2026-03-03T12:34:56Z",
        )

    assert mutate_calls == 0
    assert adapter.calls == [(current_state, authority_response)]
    assert adapter.last_proposal is not None

    refusal_report = _build_refusal_report(
        transition_name="debit_account",
        entity_type="account",
        entity_id="acct-099",
        actor="user:ops",
        authority="policy/account-debit",
        authority_response=authority_response,
        proposal=adapter.last_proposal,
        error=exc_info.value,
        refused_at="2026-03-03T12:34:56Z",
    )
    expected_base = {
        "schema_version": "refusal-0.1",
        "status": "refused",
        "transition": "debit_account",
        "entity_type": "account",
        "entity_id": "acct-099",
        "actor": "user:ops",
        "authority": "policy/account-debit",
        "refused_at": "2026-03-03T12:34:56Z",
        "authority_response": {
            "approved_balance": -999,
            "response_id": "resp-009",
        },
        "proposal": {
            "changes": {
                "balance": -999,
            },
            "source": "authority-api/mock-ledger",
            "reference": "resp-009",
            "details": {
                "provider": "mock-authority",
                "claim": "externally approved overdraft",
            },
        },
        "reason": {
            "error": "invariant_violation",
            "failures": [
                {
                    "invariant": "non_negative_balance",
                    "code": "invariant_failed",
                    "message": "Invariant 'non_negative_balance' failed.",
                }
            ],
        },
    }
    expected_report = {**expected_base, "refusal_id": hash_canonical_json(expected_base)}

    assert refusal_report == expected_report
    assert refusal_report["refusal_id"] == hash_canonical_json(expected_base)
    assert canonical_json_dumps(refusal_report) == canonical_json_dumps(expected_report)
