import json
import shutil
import tempfile
import unittest
from pathlib import Path

from policyfaultbench.cost import CostGuard
from policyfaultbench.events import EventWriter
from policyfaultbench.policy import PolicySet
from policyfaultbench.models import ProviderResponse, ToolProposal
from policyfaultbench.providers.base import ProviderAdapter
from policyfaultbench.providers.mock import MockAdapter
from policyfaultbench.runner import ExperimentRunner
from policyfaultbench.safety import AssetManifest
from policyfaultbench.sandbox import SyntheticSandbox
from policyfaultbench.scenarios import load_scenarios
from policyfaultbench.validation import validate_project


ROOT = Path(__file__).resolve().parents[1]


class OmissionAdapter(ProviderAdapter):
    provider = "mock"

    def __init__(self):
        super().__init__("omission-test", "")

    def propose(self, scenario):
        call = scenario.proposed_call.as_dict()
        call["arguments"] = {}
        call["context"] = {"provenance": scenario.provenance}
        return ProviderResponse(
            provider=self.provider,
            model=self.model,
            text="",
            proposals=(ToolProposal("propose_action", call, "omitted"),),
            input_tokens=0,
            output_tokens=0,
            raw_stop_reason="tool_call",
        )


class ValidationRunnerTests(unittest.TestCase):
    def test_project_contract(self):
        report = validate_project(ROOT)
        self.assertTrue(report.ok)

    def test_project_contract_accepts_validated_protocol_amendment(self):
        with tempfile.TemporaryDirectory() as directory:
            amended_root = Path(directory) / "PolicyFaultBench"
            shutil.copytree(ROOT, amended_root)
            config_path = amended_root / "config" / "experiment.json"
            config = json.loads(config_path.read_text(encoding="utf-8"))
            settings = config["definitive_study"]
            settings["experiment_id"] = "pfb-definitive-v1p1"
            settings["protocol_version"] = "1.1"
            config_path.write_text(
                json.dumps(config, indent=2) + "\n",
                encoding="utf-8",
            )
            report = validate_project(amended_root)
        self.assertTrue(report.ok)

    def test_project_contract_accepts_second_protocol_amendment(self):
        with tempfile.TemporaryDirectory() as directory:
            amended_root = Path(directory) / "PolicyFaultBench"
            shutil.copytree(ROOT, amended_root)
            config_path = amended_root / "config" / "experiment.json"
            config = json.loads(config_path.read_text(encoding="utf-8"))
            settings = config["definitive_study"]
            settings["experiment_id"] = "pfb-definitive-v1p2"
            settings["protocol_version"] = "1.2"
            config_path.write_text(
                json.dumps(config, indent=2) + "\n",
                encoding="utf-8",
            )
            report = validate_project(amended_root)
        self.assertTrue(report.ok)

    def test_all_offline_scenarios_pass_oracles(self):
        policy = PolicySet.load(ROOT / "policies/reference_policy.json")
        manifest = AssetManifest.load(ROOT / "manifests/owned_assets.json")
        scenarios = load_scenarios(ROOT / "scenarios/core_40.json")
        with tempfile.TemporaryDirectory() as directory:
            runner = ExperimentRunner(
                policy, SyntheticSandbox(manifest), MockAdapter(),
                EventWriter(Path(directory) / "events.jsonl"),
                CostGuard({"mock": 0.0}, 0.0, {"mock": (0.0, 0.0)}),
            )
            outcomes = [runner.run(s) for s in scenarios]
        self.assertEqual(len(outcomes), 40)
        self.assertTrue(all(not x.aborted for x in outcomes))
        self.assertTrue(all(x.oracle_passed for x in outcomes))
        self.assertFalse(any(x.containment_failure for x in outcomes))

    def test_semantic_omission_aborts_before_policy_or_state_change(self):
        policy = PolicySet.load(ROOT / "policies/reference_policy.json")
        manifest = AssetManifest.load(ROOT / "manifests/owned_assets.json")
        scenario = next(
            item
            for item in load_scenarios(ROOT / "scenarios/core_40.json")
            if item.scenario_id == "S07-D"
        )
        with tempfile.TemporaryDirectory() as directory:
            runner = ExperimentRunner(
                policy,
                SyntheticSandbox(manifest),
                OmissionAdapter(),
                EventWriter(Path(directory) / "events.jsonl"),
                CostGuard({"mock": 0.0}, 0.0, {"mock": (0.0, 0.0)}),
            )
            outcome = runner.run(scenario)
        self.assertTrue(outcome.aborted)
        self.assertEqual(outcome.abort_class, "model_conformance")
        self.assertTrue(outcome.terminal)
        self.assertFalse(outcome.state_changed)
        self.assertFalse(outcome.containment_failure)


if __name__ == "__main__":
    unittest.main()
