from __future__ import annotations

import uuid
from datetime import timedelta
from pathlib import Path

import pytest
from conftest import make_runtime

from scopewrit.crypto import sign_envelope
from scopewrit.errors import LedgerError, ReplayError, SignatureError, StateError
from scopewrit.ledger import verify_audit_packet
from scopewrit.models import ActionProposal, Decision, Effect, ScopeManifest, ScopeRule


def _http_proposal(runtime, request_id: str, resource: str) -> ActionProposal:
    return ActionProposal(
        request_id=request_id,
        run_id=runtime.manifest.run_id,
        action="network.http",
        resource=resource,
        method="GET",
    )


def test_same_nonce_cannot_be_reissued_under_a_different_run(runtime) -> None:
    payload = runtime.manifest.model_dump(mode="json")
    payload["run_id"] = f"run-{uuid.uuid4().hex}"
    reissued = ScopeManifest.model_validate(payload)
    envelope = sign_envelope(
        reissued.model_dump(mode="json"),
        runtime.principal_private,
        "principal-key",
    )
    with pytest.raises(ReplayError):
        runtime.engine.start_run(envelope)


def test_ticket_expires_and_cannot_survive_run_close(runtime) -> None:
    first = _http_proposal(
        runtime,
        "ticket-expiry",
        "https://example.com/authorized/value",
    )
    first_decision = runtime.engine.authorize(
        attestation=runtime.attestation,
        proposal=first,
    )
    runtime.clock.value += timedelta(seconds=31)
    with pytest.raises(SignatureError, match="expired"):
        runtime.engine.consume_ticket(
            first_decision.ticket,
            proposal=first,
            adapter="http",
        )

    second = _http_proposal(
        runtime,
        "ticket-closed-run",
        "https://example.com/authorized/other",
    )
    second_decision = runtime.engine.authorize(
        attestation=runtime.attestation,
        proposal=second,
    )
    runtime.engine.close_run(runtime.manifest.run_id)
    with pytest.raises(StateError, match="not active"):
        runtime.engine.consume_ticket(
            second_decision.ticket,
            proposal=second,
            adapter="http",
        )


def test_ticket_cannot_cross_adapter_boundary(runtime) -> None:
    action = _http_proposal(
        runtime,
        "wrong-adapter",
        "https://example.com/authorized/value",
    )
    decision = runtime.engine.authorize(
        attestation=runtime.attestation,
        proposal=action,
    )
    with pytest.raises(SignatureError, match="adapter binding"):
        runtime.engine.consume_ticket(
            decision.ticket,
            proposal=action,
            adapter="mcp",
        )


def test_private_deny_rule_matches_without_granting_private_authority(
    tmp_path: Path,
) -> None:
    runtime = make_runtime(
        tmp_path / "runtime",
        rules=(
            ScopeRule(
                rule_id="deny-loopback",
                effect=Effect.DENY,
                action="network.http",
                resource="http://127.0.0.1:9999/**",
                methods=("GET",),
            ),
            ScopeRule(
                rule_id="allow-all-network",
                effect=Effect.ALLOW,
                action="network.http",
                resource="http://127.0.0.1:9999/**",
                methods=("GET",),
                allow_private_network=True,
                resolved_ip_cidrs=("127.0.0.0/8",),
            ),
        ),
    )
    proposal = ActionProposal(
        request_id="private-explicit-deny",
        run_id=runtime.manifest.run_id,
        action="network.http",
        resource="http://127.0.0.1:9999/value",
        method="GET",
        attributes={"resolved_ips": ["127.0.0.1"]},
    )
    decision = runtime.engine.authorize(
        attestation=runtime.attestation,
        proposal=proposal,
    )
    assert decision.decision == Decision.DENY
    assert decision.matched_rule_id == "deny-loopback"


def test_signed_packet_detects_valid_prefix_truncation(runtime) -> None:
    action = _http_proposal(
        runtime,
        "packet-truncation",
        "https://example.com/authorized/value",
    )
    decision = runtime.engine.authorize(
        attestation=runtime.attestation,
        proposal=action,
    )
    runtime.engine.consume_ticket(
        decision.ticket,
        proposal=action,
        adapter="http",
    )
    runtime.engine.record_execution(proposal=action, outcome={"status": "not_executed", "reason": "token-consumption control"})
    packet = runtime.engine.close_run(runtime.manifest.run_id)
    ledger_path = (
        runtime.engine.data_dir / "runs" / runtime.manifest.run_id / "flight-recorder.jsonl"
    )
    lines = ledger_path.read_text(encoding="utf-8").splitlines()
    ledger_path.write_text("\n".join(lines[:-1]) + "\n", encoding="utf-8")
    with pytest.raises(LedgerError, match="ledger head"):
        verify_audit_packet(
            packet,
            runtime.engine.issuer_trust,
            ledger_path,
        )


def test_oversized_token_is_rejected_before_decode(runtime) -> None:
    action = _http_proposal(
        runtime,
        "oversized-token",
        "https://example.com/authorized/value",
    )
    with pytest.raises(SignatureError, match="16 KiB"):
        runtime.engine.authorize(
            attestation="x" * (16 * 1024 + 1),
            proposal=action,
        )
