from __future__ import annotations

import pytest

from icsd.core.errors import InvariantViolationError
from icsd.core.evidence import AuthoritySource, EvidenceBundle
from icsd.core.invariants import Invariant, InvariantProfile, InvariantProfileRegistry, InvariantSet
from icsd.core.transition import Transition


def _construction_invariants() -> InvariantSet:
    return InvariantSet(
        [
            Invariant(
                name="tolerance_window_valid",
                check=lambda state: (
                    float(state["tolerance_min_mm"]) < float(state["tolerance_max_mm"])
                ),
            ),
            Invariant(
                name="accepted_within_tolerance",
                check=lambda state: (
                    not bool(state.get("accepted"))
                    or float(state["tolerance_min_mm"])
                    <= float(state["measurement_mm"])
                    <= float(state["tolerance_max_mm"])
                ),
            ),
            Invariant(
                name="accepted_requires_inspector",
                check=lambda state: (
                    not bool(state.get("accepted"))
                    or bool(str(state.get("inspected_by", "")).strip())
                ),
            ),
        ]
    )


def _construction_registry() -> InvariantProfileRegistry:
    return InvariantProfileRegistry(
        [
            InvariantProfile(
                name="construction-qa",
                policy="standard/construction-tolerance",
                policy_version="2026.03",
                invariants=_construction_invariants(),
            )
        ]
    )


def _policy_provenance(evidence: EvidenceBundle) -> dict[str, object]:
    entry = next(item for item in evidence.provenance if item.source.startswith("policy-profile/"))
    return entry.as_dict()


def _base_component_state(*, measurement: float) -> dict[str, object]:
    return _construction_invariants().construct_state(
        {
            "component_id": "COMP-CONF-001",
            "measurement_mm": measurement,
            "tolerance_min_mm": 9.95,
            "tolerance_max_mm": 10.20,
            "accepted": False,
            "inspected_by": "",
        }
    )


def test_construction_conformance_acceptance_path_records_integrity_and_provenance() -> None:
    measure_transition = Transition(
        name="record_measurement",
        mutate=lambda state: state.update({"measurement_mm": 10.08}),
        profile_registry=_construction_registry(),
    )
    accept_transition = Transition(
        name="accept_component",
        mutate=lambda state: state.update({"accepted": True, "inspected_by": "inspector:n.owen"}),
        profile_registry=_construction_registry(),
    )

    measured_context = measure_transition.build_context(
        profile="construction-qa",
        policy_version="2026.03",
        authority_sources=[
            AuthoritySource(
                source_type="sensor",
                source_id="caliper-station-03",
                reference="measurement/COMP-CONF-001/step-1",
                details={
                    "operator": "technician:lea",
                    "measurement_id": "meas-COMP-CONF-001-0001",
                },
            )
        ],
    )
    measured = measure_transition.apply_with_context(
        state=_base_component_state(measurement=10.00),
        entity_type="manufactured_component",
        entity_id="COMP-CONF-001",
        actor="technician:lea",
        context=measured_context,
        timestamp="2026-03-05T08:30:00Z",
    )

    accepted_context = accept_transition.build_context(
        profile="construction-qa",
        policy_version="2026.03",
        authority_sources=[
            AuthoritySource(
                source_type="inspector",
                source_id="qa-team-a",
                reference="inspection/COMP-CONF-001/final",
                details={
                    "inspector_id": "n.owen",
                    "inspection_badge": "QA-17",
                },
            )
        ],
    )
    accepted = accept_transition.apply_with_context(
        state=measured.after_state,
        entity_type="manufactured_component",
        entity_id="COMP-CONF-001",
        actor="inspector:n.owen",
        context=accepted_context,
        timestamp="2026-03-05T09:00:00Z",
    )
    assert accepted.after_state["accepted"] is True
    assert accepted.evidence.verify_integrity(
        before_state=accepted.before_state,
        after_state=accepted.after_state,
    )
    provenance = _policy_provenance(accepted.evidence)
    assert provenance["reference"] == "standard/construction-tolerance@2026.03"
    assert provenance["details"]["policy_version"] == "2026.03"
    assert measured.evidence.authority_sources[0].source_type == "sensor"
    assert (
        measured.evidence.authority_sources[0].details["measurement_id"]
        == "meas-COMP-CONF-001-0001"
    )
    assert accepted.evidence.authority_sources[0].source_type == "inspector"
    assert accepted.evidence.authority_sources[0].details["inspector_id"] == "n.owen"


def test_construction_conformance_rejects_out_of_tolerance_acceptance() -> None:
    accept_transition = Transition(
        name="accept_component",
        mutate=lambda state: state.update({"accepted": True, "inspected_by": "inspector:n.owen"}),
        profile_registry=_construction_registry(),
    )
    context = accept_transition.build_context(
        profile="construction-qa",
        policy_version="2026.03",
        authority_sources=[
            AuthoritySource(
                source_type="inspector",
                source_id="qa-team-a",
                reference="inspection/COMP-CONF-001/final",
            )
        ],
    )

    with pytest.raises(InvariantViolationError):
        accept_transition.apply_with_context(
            state=_base_component_state(measurement=10.45),
            entity_type="manufactured_component",
            entity_id="COMP-CONF-001",
            actor="inspector:n.owen",
            context=context,
            timestamp="2026-03-05T09:00:00Z",
        )


def test_construction_conformance_rejects_acceptance_without_inspector_source() -> None:
    def require_inspector(authority: str, sources: tuple[AuthoritySource, ...]) -> None:
        if not sources or all(source.source_type != "inspector" for source in sources):
            msg = f"inspection authority validation failed for {authority!r}."
            raise ValueError(msg)

    accept_transition = Transition(
        name="accept_component",
        mutate=lambda state: state.update({"accepted": True, "inspected_by": "inspector:n.owen"}),
        profile_registry=_construction_registry(),
        authority_validator=require_inspector,
    )
    with pytest.raises(ValueError, match="inspection authority validation failed"):
        accept_transition.build_context(
            profile="construction-qa",
            policy_version="2026.03",
            authority_sources=[
                AuthoritySource(
                    source_type="sensor",
                    source_id="caliper-station-03",
                    reference="measurement/COMP-CONF-001/step-2",
                )
            ],
        )


def test_construction_conformance_supports_survivable_forks_with_disjoint_sources() -> None:
    transition = Transition(
        name="record_measurement",
        mutate=lambda state: state.update({"measurement_mm": state["measurement_mm"] + 0.09}),
        profile_registry=_construction_registry(),
    )
    initial = _base_component_state(measurement=10.00)

    context_a = transition.build_context(
        profile="construction-qa",
        policy_version="2026.03",
        authority_sources=[
            AuthoritySource(
                source_type="sensor",
                source_id="caliper-station-a",
                reference="measurement/COMP-CONF-001/a",
            )
        ],
    )
    context_b = transition.build_context(
        profile="construction-qa",
        policy_version="2026.03",
        authority_sources=[
            AuthoritySource(
                source_type="sensor",
                source_id="caliper-station-b",
                reference="measurement/COMP-CONF-001/b",
            )
        ],
    )

    branch_a = transition.apply_with_context(
        state=initial,
        entity_type="manufactured_component",
        entity_id="COMP-CONF-001",
        actor="technician:a",
        context=context_a,
        timestamp="2026-03-05T08:31:00Z",
    )
    branch_b = transition.apply_with_context(
        state=initial,
        entity_type="manufactured_component",
        entity_id="COMP-CONF-001",
        actor="technician:b",
        context=context_b,
        timestamp="2026-03-05T08:31:00Z",
    )

    assert branch_a.evidence.verify_integrity(
        before_state=branch_a.before_state,
        after_state=branch_a.after_state,
    )
    assert branch_b.evidence.verify_integrity(
        before_state=branch_b.before_state,
        after_state=branch_b.after_state,
    )
    assert (
        branch_a.evidence.authority_sources[0].source_id
        != branch_b.evidence.authority_sources[0].source_id
    )
