from __future__ import annotations

from datetime import datetime, timezone

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

LAB_STAGES = {"draft", "sample_collected", "transferred", "analysed", "result_released"}


def _parse_utc_timestamp(raw_value: str) -> datetime:
    value = raw_value.strip()
    if value.endswith("Z"):
        value = f"{value[:-1]}+00:00"
    parsed = datetime.fromisoformat(value)
    return parsed.astimezone(timezone.utc)


def _calibration_age_hours(state: dict[str, object]) -> float:
    calibrated_at = str(state.get("instrument_last_calibrated_at", "")).strip()
    analysis_at = str(state.get("analysis_at", "")).strip()
    if not calibrated_at or not analysis_at:
        return -1.0
    calibrated = _parse_utc_timestamp(calibrated_at)
    analysed = _parse_utc_timestamp(analysis_at)
    return (analysed - calibrated).total_seconds() / 3600.0


def _custody_chain_valid(state: dict[str, object]) -> bool:
    chain = state.get("custody_chain", [])
    if not isinstance(chain, list):
        return False
    if any(not isinstance(holder, str) or not holder.strip() for holder in chain):
        return False
    stage = state.get("stage")
    if stage == "draft":
        return len(chain) == 0
    if not chain:
        return False
    if str(state.get("current_holder", "")).strip() != chain[-1]:
        return False
    if stage in {"transferred", "analysed", "result_released"}:
        if len(chain) < 2:
            return False
        if str(state.get("previous_holder", "")).strip() != chain[-2]:
            return False
        if not str(state.get("custody_actor", "")).strip():
            return False
    return True


def _lab_invariants(*, max_calibration_age_hours: float, version: str) -> InvariantSet:
    window_name = f"instrument_calibrated_within_window_{version}"
    return InvariantSet(
        [
            Invariant(
                name=f"stage_known_{version}",
                check=lambda state: state.get("stage") in LAB_STAGES,
            ),
            Invariant(name="custody_chain_continuous", check=_custody_chain_valid),
            Invariant(
                name="analysis_requires_actor",
                check=lambda state: (
                    state.get("stage") not in {"analysed", "result_released"}
                    or bool(str(state.get("analysed_by", "")).strip())
                ),
            ),
            Invariant(
                name="calibration_not_after_analysis",
                check=lambda state: (
                    state.get("stage") not in {"analysed", "result_released"}
                    or (
                        _calibration_age_hours(state) >= 0
                        and bool(str(state.get("instrument_last_calibrated_at", "")).strip())
                        and bool(str(state.get("analysis_at", "")).strip())
                    )
                ),
            ),
            Invariant(
                name=window_name,
                check=lambda state: (
                    state.get("stage") not in {"analysed", "result_released"}
                    or (
                        _calibration_age_hours(state) >= 0
                        and _calibration_age_hours(state) <= max_calibration_age_hours
                    )
                ),
            ),
            Invariant(
                name="released_requires_result_reference",
                check=lambda state: (
                    state.get("stage") != "result_released"
                    or (
                        bool(str(state.get("released_by", "")).strip())
                        and bool(str(state.get("result_reference", "")).strip())
                    )
                ),
            ),
        ]
    )


def _lab_profile_registry() -> InvariantProfileRegistry:
    return InvariantProfileRegistry(
        [
            InvariantProfile(
                name="lab-sample",
                policy="policy/lab-calibration-window",
                policy_version="2026.03-morning",
                active_from="2026-03-09T00:00:00Z",
                active_until="2026-03-09T12:00:00Z",
                invariants=_lab_invariants(
                    max_calibration_age_hours=8.0,
                    version="202603_morning",
                ),
            ),
            InvariantProfile(
                name="lab-sample",
                policy="policy/lab-calibration-window",
                policy_version="2026.03-afternoon",
                active_from="2026-03-09T12:00:00Z",
                invariants=_lab_invariants(
                    max_calibration_age_hours=4.0,
                    version="202603_afternoon",
                ),
            ),
        ]
    )


def _base_sample_state() -> dict[str, object]:
    return _lab_invariants(max_calibration_age_hours=8.0, version="202603_morning").construct_state(
        {
            "sample_id": "LAB-CONF-001",
            "patient_ref": "patient:anon-77",
            "stage": "draft",
            "previous_holder": "",
            "current_holder": "",
            "custody_actor": "",
            "custody_chain": [],
            "instrument_id": "analyser-bio-17",
            "instrument_last_calibrated_at": "2026-03-09T08:30:00Z",
            "analysis_at": "",
            "analysed_by": "",
            "released_by": "",
            "result_reference": "",
            "result_value": "",
        }
    )


def _source(
    *,
    source_type: str,
    source_id: str,
    reference: str,
    actor_ref: str,
) -> AuthoritySource:
    return AuthoritySource(
        source_type=source_type,
        source_id=source_id,
        reference=reference,
        details={"actor_ref": actor_ref},
    )


def _require_release_authority(authority: str, sources: tuple[AuthoritySource, ...]) -> None:
    if not sources or all(source.source_type != "lab-supervisor" for source in sources):
        msg = (
            f"release authority validation failed for {authority!r}: "
            "expected at least one source_type='lab-supervisor'."
        )
        raise ValueError(msg)


def _append_holder(data: dict[str, object], next_holder: str, custody_actor: str) -> None:
    chain = data.get("custody_chain", [])
    if not isinstance(chain, list):
        chain = []
    previous_holder = str(data.get("current_holder", "")).strip()
    data.update(
        {
            "previous_holder": previous_holder,
            "current_holder": next_holder,
            "custody_actor": custody_actor,
            "custody_chain": [*chain, next_holder],
        }
    )


def _mutate_collected(data: dict[str, object]) -> None:
    data.update(
        {
            "stage": "sample_collected",
            "current_holder": "collector:nurse-amy",
            "custody_actor": "collector:nurse-amy",
            "custody_chain": ["collector:nurse-amy"],
        }
    )


def _mutate_transferred(data: dict[str, object]) -> None:
    data.update({"stage": "transferred"})
    _append_holder(data, "lab:receiving-bench", "courier:specimen-runner-3")


def _mutate_analysed(data: dict[str, object]) -> None:
    data.update(
        {
            "stage": "analysed",
            "analysis_at": "2026-03-09T11:30:00Z",
            "analysed_by": "analyst:lee",
            "result_value": "negative",
        }
    )
    _append_holder(data, "lab:analysis-bench-2", "analyst:lee")


def _mutate_release_stale(data: dict[str, object]) -> None:
    data.update(
        {
            "stage": "result_released",
            "analysis_at": "2026-03-09T13:00:00Z",
            "released_by": "pathologist:khan",
            "result_reference": "result/LAB-CONF-001/v1",
        }
    )
    _append_holder(data, "lab:results-desk", "pathologist:khan")


def _mutate_release_recalibrated(data: dict[str, object]) -> None:
    data.update(
        {
            "stage": "result_released",
            "instrument_last_calibrated_at": "2026-03-09T12:45:00Z",
            "analysis_at": "2026-03-09T13:00:00Z",
            "analysed_by": "analyst:lee",
            "released_by": "pathologist:khan",
            "result_reference": "result/LAB-CONF-001/v2",
        }
    )
    _append_holder(data, "lab:results-desk", "pathologist:khan")


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 test_lab_conformance_records_custody_calibration_and_policy_cutover() -> None:
    registry = _lab_profile_registry()

    collected_transition = Transition(
        name="sample_collected",
        mutate=_mutate_collected,
        profile_registry=registry,
        default_profile="lab-sample",
        authority_validator=Transition.require_authority_sources(minimum=1),
    )
    transferred_transition = Transition(
        name="transferred",
        mutate=_mutate_transferred,
        profile_registry=registry,
        default_profile="lab-sample",
        authority_validator=Transition.require_authority_sources(minimum=1),
    )
    analysed_transition = Transition(
        name="analysed",
        mutate=_mutate_analysed,
        profile_registry=registry,
        default_profile="lab-sample",
        authority_validator=Transition.require_authority_sources(minimum=1),
    )
    released_transition = Transition(
        name="result_released",
        mutate=_mutate_release_recalibrated,
        profile_registry=registry,
        default_profile="lab-sample",
        authority_validator=_require_release_authority,
    )

    collected_context = collected_transition.build_context(
        profile="lab-sample",
        policy_version="2026.03-morning",
        active_at="2026-03-09T09:00:00Z",
        authority_sources=[
            _source(
                source_type="collection-log",
                source_id="lab-intake-terminal",
                reference="samples/LAB-CONF-001/collect",
                actor_ref="collector:nurse-amy",
            )
        ],
    )
    collected = collected_transition.apply_with_context(
        state=_base_sample_state(),
        entity_type="lab_sample",
        entity_id="LAB-CONF-001",
        actor="collector:nurse-amy",
        context=collected_context,
        timestamp="2026-03-09T09:00:00Z",
    )

    transferred_context = transferred_transition.build_context(
        profile="lab-sample",
        policy_version="2026.03-morning",
        active_at="2026-03-09T10:00:00Z",
        authority_sources=[
            _source(
                source_type="transfer-log",
                source_id="specimen-courier-console",
                reference="samples/LAB-CONF-001/transfer",
                actor_ref="courier:specimen-runner-3",
            )
        ],
    )
    transferred = transferred_transition.apply_with_context(
        state=collected.after_state,
        entity_type="lab_sample",
        entity_id="LAB-CONF-001",
        actor="courier:specimen-runner-3",
        context=transferred_context,
        timestamp="2026-03-09T10:00:00Z",
    )

    analysed_context = analysed_transition.build_context(
        profile="lab-sample",
        policy_version="2026.03-morning",
        active_at="2026-03-09T11:30:00Z",
        authority_sources=[
            _source(
                source_type="analysis-log",
                source_id="analyser-bio-17",
                reference="samples/LAB-CONF-001/analysis-1",
                actor_ref="analyst:lee",
            )
        ],
    )
    analysed = analysed_transition.apply_with_context(
        state=transferred.after_state,
        entity_type="lab_sample",
        entity_id="LAB-CONF-001",
        actor="analyst:lee",
        context=analysed_context,
        timestamp="2026-03-09T11:30:00Z",
    )

    released_context = released_transition.build_context(
        profile="lab-sample",
        policy_version="2026.03-afternoon",
        active_at="2026-03-09T13:10:00Z",
        authority_sources=[
            _source(
                source_type="lab-supervisor",
                source_id="shift-supervisor",
                reference="release/LAB-CONF-001/signed",
                actor_ref="pathologist:khan",
            )
        ],
    )
    released = released_transition.apply_with_context(
        state=analysed.after_state,
        entity_type="lab_sample",
        entity_id="LAB-CONF-001",
        actor="pathologist:khan",
        context=released_context,
        timestamp="2026-03-09T13:10:00Z",
    )

    assert released.after_state["stage"] == "result_released"
    assert released.after_state["current_holder"] == "lab:results-desk"
    assert released.after_state["custody_chain"] == [
        "collector:nurse-amy",
        "lab:receiving-bench",
        "lab:analysis-bench-2",
        "lab:results-desk",
    ]
    assert collected.evidence.verify_integrity(
        before_state=collected.before_state,
        after_state=collected.after_state,
    )
    assert transferred.evidence.verify_integrity(
        before_state=transferred.before_state,
        after_state=transferred.after_state,
    )
    assert analysed.evidence.verify_integrity(
        before_state=analysed.before_state,
        after_state=analysed.after_state,
    )
    assert released.evidence.verify_integrity(
        before_state=released.before_state,
        after_state=released.after_state,
    )

    analysed_policy = _policy_provenance(analysed.evidence)
    released_policy = _policy_provenance(released.evidence)

    assert analysed_policy["details"]["policy_version"] == "2026.03-morning"
    assert analysed_policy["details"]["active_at"] == "2026-03-09T11:30:00Z"
    assert analysed_policy["details"]["active_until"] == "2026-03-09T12:00:00Z"

    assert released_policy["details"]["policy_version"] == "2026.03-afternoon"
    assert released_policy["details"]["active_at"] == "2026-03-09T13:10:00Z"
    assert released_policy["details"]["active_from"] == "2026-03-09T12:00:00Z"
    assert "active_until" not in released_policy["details"]
    assert released.evidence.authority_sources[0].source_type == "lab-supervisor"


def test_lab_conformance_rejects_stale_calibration_and_unauthorised_release() -> None:
    registry = _lab_profile_registry()

    collected_transition = Transition(
        name="sample_collected",
        mutate=_mutate_collected,
        profile_registry=registry,
        default_profile="lab-sample",
        authority_validator=Transition.require_authority_sources(minimum=1),
    )
    transferred_transition = Transition(
        name="transferred",
        mutate=_mutate_transferred,
        profile_registry=registry,
        default_profile="lab-sample",
        authority_validator=Transition.require_authority_sources(minimum=1),
    )
    analysed_transition = Transition(
        name="analysed",
        mutate=_mutate_analysed,
        profile_registry=registry,
        default_profile="lab-sample",
        authority_validator=Transition.require_authority_sources(minimum=1),
    )
    release_transition = Transition(
        name="result_released",
        mutate=_mutate_release_stale,
        profile_registry=registry,
        default_profile="lab-sample",
        authority_validator=_require_release_authority,
    )

    collected = collected_transition.apply_with_context(
        state=_base_sample_state(),
        entity_type="lab_sample",
        entity_id="LAB-CONF-001",
        actor="collector:nurse-amy",
        context=collected_transition.build_context(
            profile="lab-sample",
            policy_version="2026.03-morning",
            active_at="2026-03-09T09:00:00Z",
            authority_sources=[
                _source(
                    source_type="collection-log",
                    source_id="lab-intake-terminal",
                    reference="samples/LAB-CONF-001/collect",
                    actor_ref="collector:nurse-amy",
                )
            ],
        ),
        timestamp="2026-03-09T09:00:00Z",
    )
    transferred = transferred_transition.apply_with_context(
        state=collected.after_state,
        entity_type="lab_sample",
        entity_id="LAB-CONF-001",
        actor="courier:specimen-runner-3",
        context=transferred_transition.build_context(
            profile="lab-sample",
            policy_version="2026.03-morning",
            active_at="2026-03-09T10:00:00Z",
            authority_sources=[
                _source(
                    source_type="transfer-log",
                    source_id="specimen-courier-console",
                    reference="samples/LAB-CONF-001/transfer",
                    actor_ref="courier:specimen-runner-3",
                )
            ],
        ),
        timestamp="2026-03-09T10:00:00Z",
    )
    analysed = analysed_transition.apply_with_context(
        state=transferred.after_state,
        entity_type="lab_sample",
        entity_id="LAB-CONF-001",
        actor="analyst:lee",
        context=analysed_transition.build_context(
            profile="lab-sample",
            policy_version="2026.03-morning",
            active_at="2026-03-09T11:30:00Z",
            authority_sources=[
                _source(
                    source_type="analysis-log",
                    source_id="analyser-bio-17",
                    reference="samples/LAB-CONF-001/analysis-1",
                    actor_ref="analyst:lee",
                )
            ],
        ),
        timestamp="2026-03-09T11:30:00Z",
    )

    stale_release_context = release_transition.build_context(
        profile="lab-sample",
        policy_version="2026.03-afternoon",
        active_at="2026-03-09T13:05:00Z",
        authority_sources=[
            _source(
                source_type="lab-supervisor",
                source_id="shift-supervisor",
                reference="release/LAB-CONF-001/signed",
                actor_ref="pathologist:khan",
            )
        ],
    )
    with pytest.raises(InvariantViolationError) as exc_info:
        release_transition.apply_with_context(
            state=analysed.after_state,
            entity_type="lab_sample",
            entity_id="LAB-CONF-001",
            actor="pathologist:khan",
            context=stale_release_context,
            timestamp="2026-03-09T13:05:00Z",
        )
    assert any(
        failure.invariant.startswith("instrument_calibrated_within_window")
        for failure in exc_info.value.failures
    )

    with pytest.raises(ValueError, match="release authority validation failed"):
        release_transition.build_context(
            profile="lab-sample",
            policy_version="2026.03-afternoon",
            active_at="2026-03-09T13:10:00Z",
            authority_sources=[
                _source(
                    source_type="analysis-log",
                    source_id="analyser-bio-17",
                    reference="release/LAB-CONF-001/attempt-unauthorised",
                    actor_ref="analyst:lee",
                )
            ],
        )
