import importlib.util
import json
import sys
import unittest
from datetime import datetime, timezone
from pathlib import Path
from tempfile import TemporaryDirectory


SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "execution_target_policy.py"
SPEC = importlib.util.spec_from_file_location("execution_target_policy_test", SCRIPT)
policy = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
sys.modules[SPEC.name] = policy
SPEC.loader.exec_module(policy)


def catalog_value(*, windows: bool = False) -> dict:
    targets = {
        "target-a": {
            "agent": "runner-a",
            "model": "model-a",
            "execution_class": "local_model",
            "selfcheck_required": True,
            "runtime": {
                "command": ["runner-a", "--model", "{model}", "{prompt}"],
                "resume_command": [
                    "runner-a",
                    "--resume",
                    "{resume_session}",
                    "--session-dir",
                    "{resume_session_dir}",
                    "{prompt}",
                ],
                "output_format": "jsonl",
                "native_session_monitor": True,
                "session_path": "sessions/*{session_id}*.jsonl",
            },
        },
        "target-b": {
            "agent": "runner-b",
            "model": "model-b",
            "execution_class": "cloud_model",
            "runtime": {"command": ["runner-b", "{prompt}"]},
        },
    }
    routes = {"worker": {}, "review": {}}
    for stage in routes:
        for lane in ("local", "cloud"):
            for grade in range(1, 11):
                route = {
                    "candidates": ["target-a", "target-b"],
                    "rule_id": f"{stage}-{lane}-g{grade:02d}",
                    "policy_priority": grade,
                    "reason_codes": ["catalog-route"],
                }
                routes[stage][f"{lane}-G{grade:02d}"] = route
    if windows:
        routes["worker"]["local-G07"] = {
            "windows": [
                {
                    "timezone": "UTC",
                    "start": "00:00",
                    "end": "12:00",
                    "candidates": ["target-a", "target-b"],
                    "rule_id": "day-route",
                },
                {
                    "timezone": "UTC",
                    "start": "12:00",
                    "end": "00:00",
                    "candidates": ["target-b", "target-a"],
                    "rule_id": "night-route",
                },
            ]
        }
    return {"schema_version": "1.0", "targets": targets, "routes": routes}


def write_catalog(root: Path, value: dict | None = None) -> Path:
    path = root / "catalog.json"
    path.write_text(json.dumps(value or catalog_value()), encoding="utf-8")
    return path


class ExecutionTargetPolicyTests(unittest.TestCase):
    def test_catalog_is_runtime_loaded_and_route_is_complete(self):
        with TemporaryDirectory() as tmp:
            catalog = policy.load_catalog(write_catalog(Path(tmp)))
            decision = policy.select_policy(
                catalog=catalog,
                stage="worker",
                lane="cloud",
                grade=3,
                evaluated_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
            )
        self.assertEqual(decision.route_id, "cloud-G03")
        self.assertEqual([item.catalog_id for item in decision.candidates], ["target-a", "target-b"])
        self.assertEqual(decision.candidates[0].agent, "runner-a")
        self.assertEqual(decision.candidates[0].model, "model-a")
        self.assertEqual(decision.catalog_revision, catalog.revision)

    def test_common_policy_has_no_built_in_catalog(self):
        self.assertFalse(hasattr(policy, "CANONICAL_TARGETS"))
        self.assertFalse(hasattr(policy, "quota_probe_spec"))
        self.assertFalse(hasattr(policy, "promotion_target"))

    def test_optional_windows_are_catalog_owned_and_timezone_generic(self):
        with TemporaryDirectory() as tmp:
            catalog = policy.load_catalog(write_catalog(Path(tmp), catalog_value(windows=True)))
            morning = policy.select_policy(
                catalog=catalog,
                stage="worker",
                lane="local",
                grade=7,
                evaluated_at=datetime(2026, 1, 1, 6, tzinfo=timezone.utc),
            )
            evening = policy.select_policy(
                catalog=catalog,
                stage="worker",
                lane="local",
                grade=7,
                evaluated_at=datetime(2026, 1, 1, 18, tzinfo=timezone.utc),
            )
        self.assertEqual(morning.candidates[0].catalog_id, "target-a")
        self.assertEqual(evening.candidates[0].catalog_id, "target-b")
        self.assertEqual(morning.rule_id, "day-route")
        self.assertEqual(evening.rule_id, "night-route")

    def test_catalog_requires_every_stage_lane_grade_route(self):
        value = catalog_value()
        del value["routes"]["review"]["cloud-G10"]
        with TemporaryDirectory() as tmp:
            with self.assertRaisesRegex(policy.CatalogError, "cover local/cloud G01..G10 exactly"):
                policy.load_catalog(write_catalog(Path(tmp), value))

    def test_unknown_target_and_unknown_template_field_are_rejected(self):
        unknown_target = catalog_value()
        unknown_target["routes"]["worker"]["local-G01"]["candidates"] = ["missing"]
        bad_template = catalog_value()
        bad_template["targets"]["target-a"]["runtime"]["command"] = ["runner", "{provider_secret}"]
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            with self.assertRaisesRegex(policy.CatalogError, "unknown targets"):
                policy.load_catalog(write_catalog(root, unknown_target))
            with self.assertRaisesRegex(policy.CatalogError, "unsupported template field"):
                policy.load_catalog(write_catalog(root, bad_template))

    def test_reasoning_effort_is_catalog_owned_and_template_bound(self):
        valid = catalog_value()
        valid["targets"]["target-b"]["reasoning_effort"] = "high"
        valid["targets"]["target-b"]["runtime"]["command"] = [
            "runner-b",
            "--effort",
            "{reasoning_effort}",
            "{prompt}",
        ]
        missing_value = catalog_value()
        missing_value["targets"]["target-b"]["runtime"]["command"] = [
            "runner-b",
            "{reasoning_effort}",
            "{prompt}",
        ]
        missing_environment_value = catalog_value()
        missing_environment_value["targets"]["target-b"]["runtime"][
            "environment"
        ] = {"MODEL_EFFORT": "{reasoning_effort}"}
        unused_value = catalog_value()
        unused_value["targets"]["target-b"]["reasoning_effort"] = "high"
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            loaded = policy.load_catalog(write_catalog(root, valid))
            self.assertEqual(loaded.targets["target-b"].reasoning_effort, "high")
            with self.assertRaisesRegex(
                policy.CatalogError, "uses reasoning_effort without"
            ):
                policy.load_catalog(write_catalog(root, missing_value))
            with self.assertRaisesRegex(
                policy.CatalogError, "uses reasoning_effort without"
            ):
                policy.load_catalog(
                    write_catalog(root, missing_environment_value)
                )
            with self.assertRaisesRegex(
                policy.CatalogError, "reasoning_effort requires"
            ):
                policy.load_catalog(write_catalog(root, unused_value))

    def test_command_executable_must_be_literal_for_preflight(self):
        value = catalog_value()
        value["targets"]["target-a"]["runtime"]["command"] = [
            "{workspace}",
            "{prompt}",
        ]
        with TemporaryDirectory() as tmp:
            with self.assertRaisesRegex(policy.CatalogError, "executable must be a literal"):
                policy.load_catalog(write_catalog(Path(tmp), value))

    def test_native_session_monitor_requires_resume_command_and_session_path(self):
        for missing_field in ("resume_command", "session_path"):
            value = catalog_value()
            del value["targets"]["target-a"]["runtime"][missing_field]
            with (
                self.subTest(missing_field=missing_field),
                TemporaryDirectory() as tmp,
                self.assertRaisesRegex(policy.CatalogError, missing_field),
            ):
                policy.load_catalog(write_catalog(Path(tmp), value))

    def test_terminal_success_contract_requires_jsonl_agent_end(self):
        valid = catalog_value()
        valid["targets"]["target-a"]["runtime"]["terminal_success"] = "agent_end"
        valid_turn = catalog_value()
        valid_turn["targets"]["target-a"]["runtime"]["terminal_success"] = "turn_completed"
        invalid_name = catalog_value()
        invalid_name["targets"]["target-a"]["runtime"]["terminal_success"] = "message_end"
        invalid_format = catalog_value()
        invalid_format["targets"]["target-a"]["runtime"]["terminal_success"] = "agent_end"
        invalid_format["targets"]["target-a"]["runtime"]["output_format"] = "text"
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            loaded = policy.load_catalog(write_catalog(root, valid))
            self.assertEqual(
                loaded.targets["target-a"].runtime["terminal_success"],
                "agent_end",
            )
            loaded_turn = policy.load_catalog(write_catalog(root, valid_turn))
            self.assertEqual(
                loaded_turn.targets["target-a"].runtime["terminal_success"],
                "turn_completed",
            )
            with self.assertRaisesRegex(policy.CatalogError, "must be 'agent_end' or"):
                policy.load_catalog(write_catalog(root, invalid_name))
            with self.assertRaisesRegex(policy.CatalogError, "requires output_format='jsonl'"):
                policy.load_catalog(write_catalog(root, invalid_format))

    def test_catalog_revision_changes_with_content(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            path = write_catalog(root)
            first = policy.load_catalog(path)
            changed = catalog_value()
            changed["targets"]["target-a"]["model"] = "model-a-next"
            path.write_text(json.dumps(changed), encoding="utf-8")
            second = policy.load_catalog(path)
        self.assertNotEqual(first.revision, second.revision)

    def test_invalid_route_inputs_are_rejected(self):
        with TemporaryDirectory() as tmp:
            catalog = policy.load_catalog(write_catalog(Path(tmp)))
            for values in (
                {"stage": "selfcheck", "lane": "local", "grade": 1},
                {"stage": "worker", "lane": "hybrid", "grade": 1},
                {"stage": "worker", "lane": "local", "grade": 0},
            ):
                with self.subTest(values=values), self.assertRaises(ValueError):
                    policy.select_policy(
                        catalog=catalog,
                        evaluated_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
                        **values,
                    )


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