from __future__ import annotations

import pytest

from icsd.adapters.authority_adapter import AuthorityProposal
from icsd.core.errors import InvariantViolationError
from icsd.core.evidence import AuthoritySource
from icsd.core.invariants import Invariant, InvariantSet
from icsd.core.transition import Transition

ROLE_ALLOWED_SCOPES: dict[str, set[str]] = {
    "viewer": {"records:read"},
    "analyst": {"records:read", "reports:read"},
    "operator": {"records:read", "reports:read", "tickets:write"},
    "admin": {
        "records:read",
        "reports:read",
        "tickets:write",
        "iam:write",
        "prod:deploy",
    },
}
PRIVILEGED_SCOPES = {"iam:write", "prod:deploy"}


def _scopes_from_state(state: dict[str, object]) -> set[str]:
    raw_scopes = state.get("requested_scopes", [])
    if not isinstance(raw_scopes, list):
        return set()
    return {scope for scope in raw_scopes if isinstance(scope, str)}


def _iam_invariants() -> InvariantSet:
    return InvariantSet(
        [
            Invariant(
                name="stage_known",
                check=lambda state: (
                    state.get("stage")
                    in {"draft", "access_requested", "approved", "granted", "revoked"}
                ),
            ),
            Invariant(
                name="role_known",
                check=lambda state: str(state.get("role", "")).strip() in ROLE_ALLOWED_SCOPES,
            ),
            Invariant(
                name="requested_scopes_list",
                check=lambda state: (
                    isinstance(state.get("requested_scopes", []), list)
                    and all(
                        isinstance(scope, str) and bool(scope.strip())
                        for scope in state.get("requested_scopes", [])
                    )
                ),
            ),
            Invariant(
                name="least_privilege_scope_subset",
                check=lambda state: _scopes_from_state(state).issubset(
                    ROLE_ALLOWED_SCOPES.get(str(state.get("role", "")).strip(), set())
                ),
            ),
            Invariant(
                name="approval_identity_required",
                check=lambda state: (
                    state.get("stage") not in {"approved", "granted", "revoked"}
                    or bool(str(state.get("approved_by", "")).strip())
                ),
            ),
            Invariant(
                name="grant_identity_required",
                check=lambda state: (
                    state.get("stage") not in {"granted", "revoked"}
                    or bool(str(state.get("granted_by", "")).strip())
                ),
            ),
            Invariant(
                name="revoke_identity_required",
                check=lambda state: (
                    state.get("stage") != "revoked"
                    or bool(str(state.get("revoked_by", "")).strip())
                ),
            ),
            Invariant(
                name="privileged_grant_requires_expiry",
                check=lambda state: (
                    state.get("stage") != "granted"
                    or not bool(PRIVILEGED_SCOPES.intersection(_scopes_from_state(state)))
                    or bool(str(state.get("grant_expires_at", "")).strip())
                ),
            ),
        ]
    )


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


def _base_request_state() -> dict[str, object]:
    return _iam_invariants().construct_state(
        {
            "request_id": "IAM-CONF-001",
            "user_id": "user:alex",
            "stage": "draft",
            "role": "admin",
            "requested_scopes": [],
            "justification": "",
            "approved_by": "",
            "granted_by": "",
            "revoked_by": "",
            "grant_expires_at": "",
            "revocation_reason": "",
        }
    )


class _HrIdpAuthorityAdapter:
    def propose(
        self,
        *,
        current_state: dict[str, object],
        authority_response: dict[str, object],
    ) -> AuthorityProposal:
        del current_state
        proposed_scopes = authority_response.get("proposed_scopes", [])
        if not isinstance(proposed_scopes, list):
            proposed_scopes = []
        return AuthorityProposal(
            changes={
                "role": authority_response.get("proposed_role", ""),
                "requested_scopes": list(proposed_scopes),
            },
            source="authority-api/hr-idp",
            reference=str(authority_response.get("response_id", "")),
            details={"provider": "hr-idp"},
        )


def test_iam_conformance_enforces_approval_chain_and_trustless_refusal() -> None:
    invariants = _iam_invariants()
    mutate_calls = 0

    access_requested = Transition(
        name="access_requested",
        mutate=lambda state: state.update(
            {
                "stage": "access_requested",
                "requested_scopes": ["records:read", "iam:write"],
                "justification": "On-call production access for incident response.",
            }
        ),
        invariants=invariants,
        authority_validator=Transition.require_authority_sources(minimum=1),
    )
    approved = Transition(
        name="approved",
        mutate=lambda state: state.update(
            {
                "stage": "approved",
                "approved_by": "manager:priya",
            }
        ),
        invariants=invariants,
        authority_validator=Transition.require_authority_sources(minimum=1),
    )

    def _grant_mutate(state: dict[str, object]) -> None:
        nonlocal mutate_calls
        mutate_calls += 1
        state.update(
            {
                "stage": "granted",
                "granted_by": "provisioner:svc-iam",
                "grant_expires_at": "2026-03-15T00:00:00Z",
            }
        )

    granted = Transition(
        name="granted",
        mutate=_grant_mutate,
        invariants=invariants,
        authority_validator=Transition.require_authority_sources(minimum=1),
        authority_adapter=_HrIdpAuthorityAdapter(),
    )
    revoked = Transition(
        name="revoked",
        mutate=lambda state: state.update(
            {
                "stage": "revoked",
                "revoked_by": "security:ops",
                "revocation_reason": "Incident resolved.",
            }
        ),
        invariants=invariants,
        authority_validator=Transition.require_authority_sources(minimum=1),
    )

    request_context = access_requested.build_context(
        authority="policy/iam/access-request",
        authority_sources=[
            _authority_source(
                source_type="request-system",
                source_id="iam-portal",
                reference="requests/IAM-CONF-001",
                location="tenant/eu-west-2",
                actor_ref="requester:user:alex",
            )
        ],
    )
    requested_result = access_requested.apply_with_context(
        state=_base_request_state(),
        entity_type="iam_access_request",
        entity_id="IAM-CONF-001",
        actor="requester:user:alex",
        context=request_context,
        timestamp="2026-03-08T09:00:00Z",
    )

    approve_context = approved.build_context(
        authority="policy/iam/approval",
        authority_sources=[
            _authority_source(
                source_type="approval-record",
                source_id="iam-approval-engine",
                reference="approvals/IAM-CONF-001",
                location="tenant/eu-west-2",
                actor_ref="approver:manager:priya",
            )
        ],
    )
    approved_result = approved.apply_with_context(
        state=requested_result.after_state,
        entity_type="iam_access_request",
        entity_id="IAM-CONF-001",
        actor="approver:manager:priya",
        context=approve_context,
        timestamp="2026-03-08T09:10:00Z",
    )

    grant_context = granted.build_context(
        authority="policy/iam/grant",
        authority_sources=[
            _authority_source(
                source_type="provisioner",
                source_id="iam-provisioning",
                reference="grants/IAM-CONF-001",
                location="tenant/eu-west-2",
                actor_ref="provisioner:svc-iam",
            )
        ],
    )
    with pytest.raises(InvariantViolationError) as exc_info:
        granted.apply_with_context(
            state=approved_result.after_state,
            entity_type="iam_access_request",
            entity_id="IAM-CONF-001",
            actor="provisioner:svc-iam",
            context=grant_context,
            authority_response={
                "proposed_role": "analyst",
                "proposed_scopes": ["records:read", "iam:write"],
                "response_id": "idp-claim-001",
            },
            timestamp="2026-03-08T09:20:00Z",
        )
    assert mutate_calls == 0
    assert any(
        failure.invariant == "least_privilege_scope_subset" for failure in exc_info.value.failures
    )

    granted_result = granted.apply_with_context(
        state=approved_result.after_state,
        entity_type="iam_access_request",
        entity_id="IAM-CONF-001",
        actor="provisioner:svc-iam",
        context=grant_context,
        timestamp="2026-03-08T09:25:00Z",
    )
    assert mutate_calls == 1

    revoke_context = revoked.build_context(
        authority="policy/iam/revoke",
        authority_sources=[
            _authority_source(
                source_type="security-ticket",
                source_id="secops",
                reference="tickets/SEC-2026-0311",
                location="tenant/eu-west-2",
                actor_ref="revoker:security:ops",
            )
        ],
    )
    revoked_result = revoked.apply_with_context(
        state=granted_result.after_state,
        entity_type="iam_access_request",
        entity_id="IAM-CONF-001",
        actor="revoker:security:ops",
        context=revoke_context,
        timestamp="2026-03-10T18:00:00Z",
    )

    assert revoked_result.after_state["stage"] == "revoked"
    assert requested_result.evidence.verify_integrity(
        before_state=requested_result.before_state,
        after_state=requested_result.after_state,
    )
    assert approved_result.evidence.verify_integrity(
        before_state=approved_result.before_state,
        after_state=approved_result.after_state,
    )
    assert granted_result.evidence.verify_integrity(
        before_state=granted_result.before_state,
        after_state=granted_result.after_state,
    )
    assert revoked_result.evidence.verify_integrity(
        before_state=revoked_result.before_state,
        after_state=revoked_result.after_state,
    )
    assert approved_result.evidence.authority_sources[0].source_type == "approval-record"
    assert granted_result.evidence.authority_sources[0].source_type == "provisioner"
    assert revoked_result.evidence.authority_sources[0].source_type == "security-ticket"


def test_iam_conformance_rejects_missing_approval_source_and_missing_expiry() -> None:
    invariants = _iam_invariants()

    approved_transition = Transition(
        name="approved",
        mutate=lambda state: state.update({"stage": "approved", "approved_by": "manager:priya"}),
        invariants=invariants,
        authority_validator=Transition.require_authority_sources(minimum=1),
    )
    with pytest.raises(
        ValueError,
        match="expected at least 1 authority source",
    ):
        approved_transition.build_context(
            authority="policy/iam/approval",
            authority_sources=[],
        )

    grant_without_expiry = Transition(
        name="granted",
        mutate=lambda state: state.update(
            {
                "stage": "granted",
                "granted_by": "provisioner:svc-iam",
                "grant_expires_at": "",
            }
        ),
        invariants=invariants,
        authority_validator=Transition.require_authority_sources(minimum=1),
    )
    grant_context = grant_without_expiry.build_context(
        authority="policy/iam/grant",
        authority_sources=[
            _authority_source(
                source_type="provisioner",
                source_id="iam-provisioning",
                reference="grants/IAM-CONF-002",
                location="tenant/eu-west-2",
                actor_ref="provisioner:svc-iam",
            )
        ],
    )
    approved_state = invariants.construct_state(
        {
            "request_id": "IAM-CONF-002",
            "user_id": "user:alex",
            "stage": "approved",
            "role": "admin",
            "requested_scopes": ["records:read", "iam:write"],
            "justification": "On-call production access for incident response.",
            "approved_by": "manager:priya",
            "granted_by": "",
            "revoked_by": "",
            "grant_expires_at": "",
            "revocation_reason": "",
        }
    )

    with pytest.raises(InvariantViolationError) as exc_info:
        grant_without_expiry.apply_with_context(
            state=approved_state,
            entity_type="iam_access_request",
            entity_id="IAM-CONF-002",
            actor="provisioner:svc-iam",
            context=grant_context,
            timestamp="2026-03-08T10:00:00Z",
        )
    assert any(
        failure.invariant == "privileged_grant_requires_expiry"
        for failure in exc_info.value.failures
    )
