from __future__ import annotations

import pytest

from icsd.core.evidence import AuthoritySource
from icsd.core.invariants import Invariant, InvariantSet
from icsd.core.transition import Transition


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


def _require_distinct_sources(*, minimum: int = 2):
    def _validator(authority: str, sources: tuple[AuthoritySource, ...]) -> None:
        if len(sources) < minimum:
            msg = (
                "authority validation failed for "
                f"{authority!r}: expected at least {minimum} distinct authority source(s)."
            )
            raise ValueError(msg)
        unique_sources = {f"{source.source_type}:{source.source_id}" for source in sources}
        if len(unique_sources) < minimum:
            msg = (
                "authority validation failed for "
                f"{authority!r}: expected at least {minimum} distinct authority source(s)."
            )
            raise ValueError(msg)

    return _validator


def test_no_single_anchor_validator_rejects_single_source() -> None:
    transition = Transition(
        name="debit_account",
        mutate=lambda state: state.update({"balance": state["balance"] - 20}),
        invariants=_balance_invariants(),
        authority_validator=_require_distinct_sources(minimum=2),
    )

    with pytest.raises(ValueError, match="at least 2 distinct authority source"):
        transition.apply(
            state={"balance": 100},
            entity_type="account",
            entity_id="acct-001",
            actor="user:ops",
            authority="policy/account-debit",
            authority_sources=[
                AuthoritySource(
                    source_type="registry",
                    source_id="anchor-a",
                    reference="registry/anchor-a",
                )
            ],
        )


def test_no_single_anchor_validator_rejects_duplicate_source_identity() -> None:
    transition = Transition(
        name="debit_account",
        mutate=lambda state: state.update({"balance": state["balance"] - 20}),
        invariants=_balance_invariants(),
        authority_validator=_require_distinct_sources(minimum=2),
    )

    with pytest.raises(ValueError, match="at least 2 distinct authority source"):
        transition.apply(
            state={"balance": 100},
            entity_type="account",
            entity_id="acct-001",
            actor="user:ops",
            authority="policy/account-debit",
            authority_sources=[
                AuthoritySource(
                    source_type="registry",
                    source_id="shared-anchor",
                    reference="registry/shared-anchor/1",
                ),
                AuthoritySource(
                    source_type="registry",
                    source_id="shared-anchor",
                    reference="registry/shared-anchor/2",
                ),
            ],
        )


def test_forked_transitions_continue_independently_with_disjoint_anchor_sets() -> None:
    fork_a_transition = Transition(
        name="apply_fork_update",
        mutate=lambda state: state.update({"balance": state["balance"] - 10}),
        invariants=_balance_invariants(),
        authority_validator=_require_distinct_sources(minimum=2),
    )
    fork_b_transition = Transition(
        name="apply_fork_update",
        mutate=lambda state: state.update({"balance": state["balance"] - 25}),
        invariants=_balance_invariants(),
        authority_validator=_require_distinct_sources(minimum=2),
    )

    initial_state = {"balance": 100}
    fork_a = fork_a_transition.apply(
        state=initial_state,
        entity_type="account",
        entity_id="acct-001",
        actor="user:fork-a",
        authority="policy/fork-a",
        authority_sources=[
            AuthoritySource(
                source_type="registry",
                source_id="fork-a-anchor-1",
                reference="registry/fork-a/1",
            ),
            AuthoritySource(
                source_type="policy",
                source_id="fork-a-anchor-2",
                reference="policy/fork-a/2026.03",
            ),
        ],
        transition_id="11111111-1111-4111-8111-111111111111",
    )
    fork_b = fork_b_transition.apply(
        state=initial_state,
        entity_type="account",
        entity_id="acct-001",
        actor="user:fork-b",
        authority="policy/fork-b",
        authority_sources=[
            AuthoritySource(
                source_type="registry",
                source_id="fork-b-anchor-1",
                reference="registry/fork-b/1",
            ),
            AuthoritySource(
                source_type="policy",
                source_id="fork-b-anchor-2",
                reference="policy/fork-b/2026.03",
            ),
        ],
        transition_id="22222222-2222-4222-8222-222222222222",
    )

    assert fork_a.before_state == initial_state
    assert fork_b.before_state == initial_state
    assert fork_a.after_state == {"balance": 90}
    assert fork_b.after_state == {"balance": 75}
    assert fork_a.evidence.authority == "policy/fork-a"
    assert fork_b.evidence.authority == "policy/fork-b"
    assert fork_a.evidence.verify_integrity(
        before_state=fork_a.before_state,
        after_state=fork_a.after_state,
    )
    assert fork_b.evidence.verify_integrity(
        before_state=fork_b.before_state,
        after_state=fork_b.after_state,
    )

    fork_a_source_ids = {source.source_id for source in fork_a.evidence.authority_sources}
    fork_b_source_ids = {source.source_id for source in fork_b.evidence.authority_sources}
    assert fork_a_source_ids.isdisjoint(fork_b_source_ids)


def test_fork_continuation_can_rotate_anchor_set_without_global_restart() -> None:
    transition = Transition(
        name="debit_account",
        mutate=lambda state: state.update({"balance": state["balance"] - 10}),
        invariants=_balance_invariants(),
        authority_validator=_require_distinct_sources(minimum=2),
    )

    first = transition.apply(
        state={"balance": 100},
        entity_type="account",
        entity_id="acct-001",
        actor="user:fork-a",
        authority="policy/fork-a",
        authority_sources=[
            AuthoritySource(
                source_type="registry",
                source_id="anchor-1",
                reference="registry/anchor-1",
            ),
            AuthoritySource(
                source_type="policy",
                source_id="anchor-2",
                reference="policy/anchor-2",
            ),
        ],
        transition_id="33333333-3333-4333-8333-333333333333",
    )
    second = transition.apply(
        state=first.after_state,
        entity_type="account",
        entity_id="acct-001",
        actor="user:fork-a",
        authority="policy/fork-a",
        authority_sources=[
            AuthoritySource(
                source_type="policy",
                source_id="anchor-2",
                reference="policy/anchor-2",
            ),
            AuthoritySource(
                source_type="registry",
                source_id="anchor-3",
                reference="registry/anchor-3",
            ),
        ],
        transition_id="44444444-4444-4444-8444-444444444444",
    )

    assert first.after_state == {"balance": 90}
    assert second.after_state == {"balance": 80}
    assert first.evidence.verify_integrity(
        before_state=first.before_state,
        after_state=first.after_state,
    )
    assert second.evidence.verify_integrity(
        before_state=second.before_state,
        after_state=second.after_state,
    )
    first_source_ids = {source.source_id for source in first.evidence.authority_sources}
    second_source_ids = {source.source_id for source in second.evidence.authority_sources}
    assert first_source_ids.intersection(second_source_ids) == {"anchor-2"}
