from __future__ import annotations

from datetime import timedelta
from pathlib import Path

import pytest
from conftest import make_runtime

from scopewrit.engine import ScopeWritEngine
from scopewrit.errors import ReplayError, SignatureError
from scopewrit.models import ActionProposal, Decision, Effect, ScopeRule
from scopewrit.resources import normalize_resource


def proposal(runtime, *, request_id: str, resource: str, justification: str | None = None):
    return ActionProposal(
        request_id=request_id,
        run_id=runtime.manifest.run_id,
        action="network.http",
        resource=resource,
        method="GET",
        justification=justification,
    )


def test_explicit_deny_precedes_allow_and_default_is_deny(tmp_path: Path) -> None:
    runtime = make_runtime(
        tmp_path / "runtime",
        rules=(
            ScopeRule(
                rule_id="deny-sensitive",
                effect=Effect.DENY,
                action="network.http",
                resource="https://example.com/authorized/private/**",
                methods=("GET",),
            ),
            ScopeRule(
                rule_id="allow-authorized",
                effect=Effect.ALLOW,
                action="network.http",
                resource="https://example.com/authorized/**",
                methods=("GET",),
            ),
        ),
    )
    denied = runtime.engine.authorize(
        attestation=runtime.attestation,
        proposal=proposal(
            runtime,
            request_id="request-deny",
            resource="https://example.com/authorized/private/key",
        ),
    )
    assert denied.decision == Decision.DENY
    assert denied.reasons == ("explicit_deny_rule",)

    default_denied = runtime.engine.authorize(
        attestation=runtime.attestation,
        proposal=proposal(
            runtime,
            request_id="request-default-deny",
            resource="https://outside.example/authorized/value",
        ),
    )
    assert default_denied.decision == Decision.DENY
    assert default_denied.reasons == ("no_matching_authority",)


def test_ticket_is_exact_bound_and_consume_once(runtime) -> None:
    action = proposal(
        runtime,
        request_id="request-allow",
        resource="https://example.com/authorized/value",
    )
    decision = runtime.engine.authorize(
        attestation=runtime.attestation,
        proposal=action,
    )
    assert decision.decision == Decision.ALLOW
    assert decision.ticket
    runtime.engine.consume_ticket(decision.ticket, proposal=action, adapter="http")
    with pytest.raises(ReplayError):
        runtime.engine.consume_ticket(
            decision.ticket,
            proposal=action,
            adapter="http",
        )

    other = proposal(
        runtime,
        request_id="request-other",
        resource="https://example.com/authorized/other",
    )
    with pytest.raises(SignatureError):
        runtime.engine.consume_ticket(
            decision.ticket,
            proposal=other,
            adapter="http",
        )


def test_rule_use_limit_fails_closed(tmp_path: Path) -> None:
    runtime = make_runtime(
        tmp_path / "runtime",
        rules=(
            ScopeRule(
                rule_id="one-shot",
                effect=Effect.ALLOW,
                action="network.http",
                resource="https://example.com/authorized/**",
                methods=("GET",),
                max_uses=1,
            ),
        ),
    )
    first = runtime.engine.authorize(
        attestation=runtime.attestation,
        proposal=proposal(
            runtime,
            request_id="request-first",
            resource="https://example.com/authorized/one",
        ),
    )
    second = runtime.engine.authorize(
        attestation=runtime.attestation,
        proposal=proposal(
            runtime,
            request_id="request-second",
            resource="https://example.com/authorized/two",
        ),
    )
    assert first.decision == Decision.ALLOW
    assert second.decision == Decision.DENY
    assert second.reasons == ("rule_use_limit_exhausted",)


def test_uncertainty_pauses_then_exact_approval_allows(runtime) -> None:
    action = proposal(
        runtime,
        request_id="request-uncertain",
        resource="https://example.com/authorized/value",
        justification="This is probably allowed and should be fine.",
    )
    paused = runtime.engine.authorize(
        attestation=runtime.attestation,
        proposal=action,
    )
    assert paused.decision == Decision.PAUSE
    assert "scope_uncertainty" in paused.uncertainty_signals

    normalized = normalize_resource(action.resource).value
    approval = ScopeWritEngine.create_approval(
        proposal=action,
        normalized_resource=normalized,
        private_key=runtime.principal_private,
        key_id="principal-key",
        issued_at=runtime.clock.value,
        expires_at=runtime.clock.value + timedelta(minutes=1),
    )
    allowed = runtime.engine.authorize(
        attestation=runtime.attestation,
        proposal=action,
        approval=approval,
    )
    assert allowed.decision == Decision.ALLOW

    replayed = runtime.engine.authorize(
        attestation=runtime.attestation,
        proposal=action,
        approval=approval,
    )
    assert replayed.decision == Decision.DENY
    assert replayed.reasons == ("invalid_or_replayed_approval",)


def test_expired_attestation_cannot_authorize(runtime) -> None:
    runtime.clock.value = runtime.manifest.expires_at + timedelta(seconds=1)
    with pytest.raises(SignatureError, match="expired"):
        runtime.engine.authorize(
            attestation=runtime.attestation,
            proposal=proposal(
                runtime,
                request_id="request-expired",
                resource="https://example.com/authorized/value",
            ),
        )


def test_engine_restart_reloads_verified_manifest(runtime) -> None:
    restarted = ScopeWritEngine(
        data_dir=runtime.engine.data_dir,
        principal_trust=runtime.engine.principal_trust,
        issuer_private_key=runtime.issuer_private,
        issuer_key_id="issuer-key",
        clock=runtime.clock,
    )
    decision = restarted.authorize(
        attestation=runtime.attestation,
        proposal=proposal(
            runtime,
            request_id="request-after-restart",
            resource="https://example.com/authorized/value",
        ),
    )
    assert decision.decision == Decision.ALLOW
