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


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


def catalog_value() -> dict:
    targets = {
        "first": {
            "agent": "agent-one",
            "model": "model-one",
            "execution_class": "local_model",
            "selfcheck_required": True,
            "runtime": {"command": ["agent-one", "{prompt}"]},
        },
        "second": {
            "agent": "agent-two",
            "model": "model-two",
            "execution_class": "cloud_model",
            "runtime": {"command": ["agent-two", "--model", "{model}", "{prompt}"]},
        },
    }
    routes = {"worker": {}, "review": {}}
    for stage in routes:
        for lane in ("local", "cloud"):
            for grade in range(1, 11):
                routes[stage][f"{lane}-G{grade:02d}"] = {
                    "candidates": ["first", "second"],
                    "rule_id": f"{stage}-{lane}-{grade:02d}",
                    "reason_codes": ["runtime-catalog"],
                }
    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


def write_task(
    root: Path,
    *,
    kind: str = "PLAN",
    lane: str = "cloud",
    grade: int = 5,
    task: str = "group/01_task",
    milestone_task: str | None = None,
) -> Path:
    milestone = f" milestone-task={milestone_task}" if milestone_task else ""
    path = root / f"{kind}-{lane}-G{grade:02d}.md"
    path.write_text(
        f"<!-- task={task} plan=0 tag=API{milestone} -->\n\n# Task\n",
        encoding="utf-8",
    )
    return path


class SelectorTests(unittest.TestCase):
    def test_catalog_must_be_injected(self):
        with TemporaryDirectory() as tmp, mock.patch.dict(os.environ, {}, clear=True):
            task = write_task(Path(tmp))
            with self.assertRaises(selector.SelectorInputError) as ctx:
                selector.select_execution_target(task)
        self.assertEqual(ctx.exception.code, "missing_execution_catalog")

    def test_initial_decision_contains_catalog_evidence_and_no_quota(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            catalog = write_catalog(root)
            result = selector.select_execution_target(
                write_task(root),
                catalog_path=catalog,
                evaluated_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
            )
        self.assertEqual(result["schema_version"], "2.0")
        self.assertEqual(result["selected"]["target_id"], "first")
        self.assertEqual(result["selected"]["agent"], "agent-one")
        self.assertEqual(result["selected"]["model"], "model-one")
        self.assertEqual(result["catalog"]["source"], str(catalog.resolve()))
        self.assertEqual([item["target_id"] for item in result["candidates"]], ["first", "second"])
        self.assertNotIn("quota", result)
        self.assertTrue(all("quota_status" not in item for item in result["candidates"]))

    def test_catalog_can_be_injected_by_environment(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            catalog = write_catalog(root)
            with mock.patch.dict(os.environ, {selector.CATALOG_ENV: str(catalog)}):
                result = selector.select_execution_target(write_task(root))
        self.assertEqual(result["catalog"]["source"], str(catalog.resolve()))

    def test_runtime_quota_error_moves_to_next_catalog_target(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            catalog = write_catalog(root)
            task = write_task(root)
            first = selector.select_execution_target(task, catalog_path=catalog)
            second = selector.select_execution_target(
                task,
                catalog_path=catalog,
                transition="failover",
                prior_decision=first,
                failure_class="provider-quota",
            )
        self.assertEqual(second["selected"]["target_id"], "second")
        self.assertEqual(second["transition"]["trigger"], "failover")
        self.assertEqual(second["transition"]["previous_target"]["target_id"], "first")

    def test_failover_requires_runtime_failure_and_unused_candidate(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            catalog = write_catalog(root)
            task = write_task(root)
            first = selector.select_execution_target(task, catalog_path=catalog)
            with self.assertRaises(selector.SelectorInputError) as ctx:
                selector.select_execution_target(
                    task,
                    catalog_path=catalog,
                    transition="failover",
                    prior_decision=first,
                    failure_class="generic-error",
                )
            self.assertEqual(ctx.exception.code, "unqualified_failover")
            second = selector.select_execution_target(
                task,
                catalog_path=catalog,
                transition="failover",
                prior_decision=first,
                failure_class="model-unavailable",
            )
            with self.assertRaises(selector.SelectorInputError) as ctx:
                selector.select_execution_target(
                    task,
                    catalog_path=catalog,
                    transition="failover",
                    prior_decision=second,
                    failure_class="provider-quota",
                )
        self.assertEqual(ctx.exception.code, "no_failover_candidate")

    def test_resume_pins_target_and_catalog_revision(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            catalog = write_catalog(root)
            task = write_task(root)
            first = selector.select_execution_target(task, catalog_path=catalog)
            resumed = selector.select_execution_target(
                task,
                catalog_path=catalog,
                transition="resume",
                prior_decision=first,
            )
            changed = catalog_value()
            changed["targets"]["first"]["model"] = "changed-model"
            catalog.write_text(json.dumps(changed), encoding="utf-8")
            with self.assertRaises(selector.SelectorInputError) as ctx:
                selector.select_execution_target(
                    task,
                    catalog_path=catalog,
                    transition="resume",
                    prior_decision=resumed,
                )
        self.assertTrue(resumed["decision"]["pinned"])
        self.assertEqual(ctx.exception.code, "catalog_revision_mismatch")

    def test_stage_and_milestone_header_contract(self):
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            catalog = write_catalog(root)
            review = write_task(root, kind="CODE_REVIEW", lane="local", grade=2)
            self.assertEqual(
                selector.select_execution_target(review, catalog_path=catalog)["stage"],
                "review",
            )
            with self.assertRaises(selector.SelectorInputError) as ctx:
                selector.select_execution_target(review, stage="worker", catalog_path=catalog)
            self.assertEqual(ctx.exception.code, "stage_mismatch")
            missing = write_task(root, task="m-feature/01_task")
            with self.assertRaises(selector.SelectorInputError) as ctx:
                selector.select_execution_target(missing, catalog_path=catalog)
            self.assertEqual(ctx.exception.code, "missing_milestone_task")

    def test_cli_returns_structured_catalog_error(self):
        with TemporaryDirectory() as tmp:
            task = write_task(Path(tmp))
            completed = subprocess.run(
                [sys.executable, str(SCRIPT), str(task)],
                capture_output=True,
                text=True,
                env={key: value for key, value in os.environ.items() if key != selector.CATALOG_ENV},
                check=False,
            )
        self.assertEqual(completed.returncode, 2)
        self.assertEqual(completed.stdout, "")
        self.assertEqual(json.loads(completed.stderr)["error"]["code"], "missing_execution_catalog")


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