from __future__ import annotations

import copy
import json
import subprocess
import sys
import shutil
from pathlib import Path


ROOT = Path(__file__).resolve().parent
GATE = ROOT / "approval_gate.py"
POLICY = ROOT / "authority-policy.json"
PROPOSAL = ROOT / "proposal.json"
CANDIDATE = ROOT / "release-candidate-7.json"
APPROVAL = ROOT / "approval-approved.json"
WORKFLOW = ROOT / "required-check.yml"
AT = "2026-09-13T20:00:00Z"


def run(
    tmp_path: Path, approval: dict | None = None, proposal: dict | None = None,
    policy: dict | None = None, at: str = AT, candidate_bytes: bytes | None = None,
):
    policy_path = tmp_path / "authority-policy.json"
    proposal_path = tmp_path / "proposal.json"
    approval_path = tmp_path / "approval-approved.json"
    policy_path.write_text(json.dumps(policy or json.loads(POLICY.read_text()), indent=2) + "\n")
    proposal_path.write_text(json.dumps(proposal or json.loads(PROPOSAL.read_text()), indent=2) + "\n")
    candidate_path = tmp_path / "release-candidate-7.json"
    candidate_path.write_bytes(candidate_bytes if candidate_bytes is not None else CANDIDATE.read_bytes())
    if approval is not None:
        approval_path.write_text(json.dumps(approval, indent=2) + "\n")
    elif APPROVAL.exists():
        approval_path.write_bytes(APPROVAL.read_bytes())
    shutil.copytree(ROOT / "evidence", tmp_path / "evidence", dirs_exist_ok=True)
    return subprocess.run(
        [sys.executable, str(GATE), "--policy", str(policy_path), "--proposal", str(proposal_path), "--candidate", str(candidate_path), "--approval", str(approval_path), "--test-mode", "--at", at],
        text=True,
        capture_output=True,
    )


def current() -> dict:
    return json.loads(APPROVAL.read_text())


def assert_blocked(result, code: str):
    assert result.returncode == 2
    assert f"BLOCKED code={code}" in result.stdout


def test_valid_approval_passes(tmp_path):
    result = run(tmp_path)
    assert result.returncode == 0
    assert result.stdout.startswith("PASS approval_id=APR-BILLING-2026-09-13-01")


def test_missing_record_fails_closed(tmp_path):
    result = run(tmp_path, approval={})
    assert_blocked(result, "approval_schema")


def test_merge_event_cannot_impersonate_approval(tmp_path):
    result = run(tmp_path, approval={"merged": True, "commit": "abc123"})
    assert_blocked(result, "approval_schema")


def test_wrong_authority_is_blocked(tmp_path):
    approval = current(); approval["approver"] = "unlisted-reviewer@example.test"
    result = run(tmp_path, approval=approval)
    assert_blocked(result, "wrong_authority")


def test_changed_proposal_is_blocked(tmp_path):
    proposal = json.loads(PROPOSAL.read_text()); proposal["scope"] = "billing-service@release-candidate-8"
    result = run(tmp_path, proposal=proposal)
    assert_blocked(result, "stale_artifact")


def test_changed_release_candidate_is_blocked(tmp_path):
    result = run(tmp_path, candidate_bytes=b'{"billing_formula":"subtotal * 1.20"}\n')
    assert_blocked(result, "stale_candidate")


def test_expired_approval_is_blocked(tmp_path):
    approval = current(); approval["expires_at"] = "2026-09-13T19:00:00Z"
    result = run(tmp_path, approval=approval)
    assert_blocked(result, "expired")


def test_silence_after_deadline_is_blocked(tmp_path):
    approval = current(); approval["decision"] = "PENDING"; approval["response_deadline"] = "2026-09-13T19:00:00Z"
    result = run(tmp_path, approval=approval)
    assert_blocked(result, "silence_deadline_elapsed")


def test_failed_criterion_is_blocked(tmp_path):
    approval = current(); approval["criteria"][0]["result"] = "FAIL"
    result = run(tmp_path, approval=approval)
    assert_blocked(result, "criterion_failed")


def test_revoked_approval_is_blocked(tmp_path):
    approval = current(); approval["status"] = "revoked"
    result = run(tmp_path, approval=approval)
    assert_blocked(result, "status_revoked")


def test_superseded_approval_is_blocked(tmp_path):
    approval = current(); approval["status"] = "superseded"
    result = run(tmp_path, approval=approval)
    assert_blocked(result, "status_superseded")


def test_changed_policy_is_blocked(tmp_path):
    policy = json.loads(POLICY.read_text()); policy["allowed_approvers"].append("backup@example.test")
    result = run(tmp_path, policy=policy)
    assert_blocked(result, "stale_policy")


def test_tampered_audit_chain_is_blocked(tmp_path):
    approval = current(); approval["audit_events"][0]["detail"] = "tampered"
    result = run(tmp_path, approval=approval)
    assert_blocked(result, "audit_chain")


def test_approval_cannot_apply_before_decision_time(tmp_path):
    result = run(tmp_path, at="2026-09-13T16:59:59Z")
    assert_blocked(result, "not_yet_effective")


def test_approval_expires_at_exact_boundary(tmp_path):
    result = run(tmp_path, at="2026-09-14T17:00:00Z")
    assert_blocked(result, "expired")


def test_decision_after_response_deadline_is_blocked(tmp_path):
    approval = current(); approval["decided_at"] = "2026-09-13T18:00:01Z"
    result = run(tmp_path, approval=approval)
    assert_blocked(result, "decision_after_deadline")


def test_free_text_evidence_cannot_replace_bound_artifact(tmp_path):
    approval = current(); approval["criteria"][0]["evidence"] = "x"
    result = run(tmp_path, approval=approval)
    assert_blocked(result, "criterion_evidence")


def test_authority_payload_change_outside_audit_chain_is_blocked(tmp_path):
    approval = current(); approval["action"] = "release a different billing change"
    result = run(tmp_path, approval=approval)
    assert_blocked(result, "audit_chain")


def test_criterion_evidence_change_outside_audit_chain_is_blocked(tmp_path):
    approval = current(); approval["criteria"][0]["evidence"]["method"] = "Trust an unbound comment instead."
    result = run(tmp_path, approval=approval)
    assert_blocked(result, "audit_chain")


def test_changed_evidence_file_is_blocked(tmp_path):
    evidence = tmp_path / "evidence" / "tests-current.json"
    # Build the temporary fixture once, then alter an evidence artifact before evaluation.
    run(tmp_path)
    evidence.write_text('{"result":"PASS","candidate":"different"}\n')
    result = subprocess.run(
        [sys.executable, str(GATE), "--policy", str(tmp_path / "authority-policy.json"),
         "--proposal", str(tmp_path / "proposal.json"), "--candidate", str(tmp_path / "release-candidate-7.json"), "--approval", str(tmp_path / "approval-approved.json"),
         "--test-mode", "--at", AT], text=True, capture_output=True,
    )
    assert_blocked(result, "criterion_evidence")


def test_production_rejects_test_clock_override(tmp_path):
    run(tmp_path)
    result = subprocess.run(
        [sys.executable, str(GATE), "--policy", str(tmp_path / "authority-policy.json"),
         "--proposal", str(tmp_path / "proposal.json"), "--candidate", str(tmp_path / "release-candidate-7.json"), "--approval", str(tmp_path / "approval-approved.json"),
         "--at", AT], text=True, capture_output=True,
    )
    assert_blocked(result, "test_time_override")


def test_test_mode_requires_explicit_clock(tmp_path):
    run(tmp_path)
    result = subprocess.run(
        [sys.executable, str(GATE), "--policy", str(tmp_path / "authority-policy.json"),
         "--proposal", str(tmp_path / "proposal.json"), "--candidate", str(tmp_path / "release-candidate-7.json"), "--approval", str(tmp_path / "approval-approved.json"),
         "--test-mode"], text=True, capture_output=True,
    )
    assert_blocked(result, "test_time_missing")


def test_required_check_runs_protected_base_controls_and_treats_head_as_data():
    workflow = WORKFLOW.read_text(encoding="utf-8")
    assert "pull_request_target:" in workflow
    assert "\n  pull_request:\n" not in workflow
    assert "paths:" not in workflow
    assert "ref: ${{ github.event.pull_request.base.sha }}" in workflow
    assert 'git -C trusted show "${PR_HEAD_SHA}:${CANDIDATE_PATH}"' in workflow
    assert "python3 trusted/docs/content_engine/artifacts/durable-approval-how/approval_gate.py" in workflow
    assert "python3 docs/content_engine/artifacts/durable-approval-how/approval_gate.py" not in workflow
    assert "PASS out_of_scope" in workflow


def test_pr_head_gate_mutation_cannot_forge_candidate_pass(tmp_path):
    untrusted = tmp_path / "pr-head"
    untrusted.mkdir()
    forged_gate = untrusted / "approval_gate.py"
    forged_gate.write_text(
        '#!/usr/bin/env python3\nprint("PASS forged-by-pr-head")\n', encoding="utf-8"
    )
    forged = subprocess.run(
        [sys.executable, str(forged_gate)], text=True, capture_output=True
    )
    assert forged.returncode == 0
    assert forged.stdout.strip() == "PASS forged-by-pr-head"

    changed_candidate = b'{"billing_formula":"subtotal * 1.20"}\n'
    trusted_run = tmp_path / "trusted-run"
    trusted_run.mkdir()
    trusted_result = run(trusted_run, candidate_bytes=changed_candidate)
    assert_blocked(trusted_result, "stale_candidate")
