"""Edge case tests — CLI failure, model timeout, partial artifacts, corrupt config."""

from pathlib import Path
from typing import Any

import pytest
from pydantic import ValidationError

from dpe.adapters.base import ModelAdapter
from dpe.adapters.registry import build_adapters, is_adapter_premium
from dpe.engine.config_loader import load_config
from dpe.engine.orchestrator import Orchestrator
from dpe.models.adapter import ModelRequest, ModelResult
from dpe.models.config import AdapterConfig, BudgetLimits, DPEConfig


class FailingAdapter(ModelAdapter):
    """Adapter that always fails — simulates CLI/model failure."""

    def generate(self, request: ModelRequest) -> ModelResult:
        raise RuntimeError("Simulated adapter failure")

    def healthcheck(self) -> bool:
        return False

    def estimate_usage(self, request: ModelRequest) -> dict[str, Any]:
        return {}

    def get_usage_snapshot(self) -> dict[str, Any]:
        return {}


class ThrottlingAdapter(ModelAdapter):
    """Adapter that reports throttle detection."""

    def generate(self, request: ModelRequest) -> ModelResult:
        return ModelResult(
            text="throttled response",
            adapter_name="throttling",
            role=request.role,
            throttle_detected=True,
        )

    def healthcheck(self) -> bool:
        return True

    def estimate_usage(self, request: ModelRequest) -> dict[str, Any]:
        return {}

    def get_usage_snapshot(self) -> dict[str, Any]:
        return {}


class TestAdapterFailure:
    def test_manifest_written_on_adapter_error(self, tmp_path: Path) -> None:
        cfg = DPEConfig(output_dir=str(tmp_path))
        orch = Orchestrator(config=cfg, adapters={"claude_code_cli": FailingAdapter()})
        manifest = orch.run("Test idea")
        assert "error" in manifest.stop_reason
        manifest_path = orch._registry.run_dir / "run_manifest.json"
        assert manifest_path.exists()

    def test_partial_artifacts_preserved(self, tmp_path: Path) -> None:
        """If adapter fails mid-run, earlier artifacts should still exist."""
        cfg = DPEConfig(
            simulation_mode=True,
            output_dir=str(tmp_path),
            budget=BudgetLimits(max_total_calls=3),
        )
        orch = Orchestrator(config=cfg)
        manifest = orch.run("Test idea")
        assert manifest.stop_reason == "budget_exceeded"
        assert len(manifest.artifacts) == 3


class TestThrottleDetection:
    def test_throttle_warning_recorded(self, tmp_path: Path) -> None:
        cfg = DPEConfig(output_dir=str(tmp_path))
        throttler = ThrottlingAdapter()
        orch = Orchestrator(
            config=cfg,
            adapters={
                "claude_code_cli": throttler,
                "codex_cli": throttler,
                "local": throttler,
            },
        )
        manifest = orch.run("Test idea")
        throttle_warnings = [
            w for w in manifest.warnings if "Throttle detected" in w
        ]
        assert len(throttle_warnings) > 0


class TestCorruptConfig:
    def test_invalid_json(self, tmp_path: Path) -> None:
        config_path = tmp_path / "bad.json"
        config_path.write_text("not json at all", encoding="utf-8")
        with pytest.raises(ValueError, match="Invalid JSON"):
            load_config(str(config_path))

    def test_missing_config_file(self) -> None:
        with pytest.raises(FileNotFoundError):
            load_config("/nonexistent/path/config.json")

    def test_invalid_budget_values(self) -> None:
        with pytest.raises(ValidationError):
            DPEConfig(budget={"max_total_calls": -5})

    def test_unknown_throttle_behavior(self) -> None:
        with pytest.raises(ValidationError):
            BudgetLimits(throttle_behavior="invalid_behavior")


class TestPathSafety:
    def test_run_dir_created_under_output(self, tmp_path: Path) -> None:
        cfg = DPEConfig(simulation_mode=True, output_dir=str(tmp_path))
        orch = Orchestrator(config=cfg)
        orch.run("Test idea")
        # Run dir must be under output_dir (resolve both for comparison)
        resolved_output = str(Path(str(tmp_path)).resolve())
        assert str(orch._registry.run_dir).startswith(resolved_output)

    def test_run_id_is_hex(self, tmp_path: Path) -> None:
        cfg = DPEConfig(simulation_mode=True, output_dir=str(tmp_path))
        orch = Orchestrator(config=cfg)
        assert all(c in "0123456789abcdef" for c in orch.run_id)


class TestDeterministicFolderStructure:
    def test_artifact_filenames_match_spec(self, tmp_path: Path) -> None:
        """Verify artifact filenames match the PRD specification."""
        cfg = DPEConfig(
            simulation_mode=True,
            output_dir=str(tmp_path),
            enable_cross_critique=True,
            budget=BudgetLimits(max_total_calls=15),
        )
        orch = Orchestrator(config=cfg)
        manifest = orch.run("Test idea")

        filenames = {a.filename for a in manifest.artifacts}
        expected = {
            "00_input_normalized.md",
            "10_ideation_a.md",
            "11_ideation_b.md",
            "20_cross_critique_a.md",
            "21_cross_critique_b.md",
            "30_synthesis.md",
            "40_prd_v1.md",
            "50_critique_r1.md",
            "60_prd_v2.md",
        }
        # At minimum, these should be present
        assert expected.issubset(filenames)


class TestAdapterRegistry:
    """Tests for adapter registry building and premium classification."""

    def test_simulation_mode_returns_simulation_adapter(self) -> None:
        cfg = DPEConfig(simulation_mode=True)
        adapters = build_adapters(cfg)
        assert "simulation" in adapters

    def test_default_adapters_built_without_config(self) -> None:
        cfg = DPEConfig(simulation_mode=False)
        adapters = build_adapters(cfg)
        assert "claude_code_cli" in adapters
        assert "codex_cli" in adapters
        assert "local" in adapters

    def test_adapters_built_from_config(self) -> None:
        cfg = DPEConfig(
            adapters={
                "my_claude": AdapterConfig(
                    adapter_type="claude_code_cli",
                    is_premium=True,
                ),
            }
        )
        adapters = build_adapters(cfg)
        assert "my_claude" in adapters

    def test_disabled_adapter_skipped(self) -> None:
        cfg = DPEConfig(
            adapters={
                "disabled_one": AdapterConfig(
                    adapter_type="claude_code_cli",
                    enabled=False,
                ),
            }
        )
        adapters = build_adapters(cfg)
        assert "disabled_one" not in adapters

    def test_premium_from_adapter_config(self) -> None:
        cfg = DPEConfig(
            adapters={
                "my_local": AdapterConfig(
                    adapter_type="local",
                    is_premium=True,
                ),
            }
        )
        assert is_adapter_premium("my_local", cfg) is True

    def test_premium_falls_back_to_defaults(self) -> None:
        cfg = DPEConfig()
        assert is_adapter_premium("claude_code_cli", cfg) is True
        assert is_adapter_premium("local", cfg) is False
        assert is_adapter_premium("simulation", cfg) is False

    def test_missing_adapter_raises_in_non_sim_mode(self, tmp_path: Path) -> None:
        """Non-simulation mode must fail loudly for missing adapters."""
        cfg = DPEConfig(output_dir=str(tmp_path))
        orch = Orchestrator(config=cfg, adapters={"local": FailingAdapter()})
        manifest = orch.run("Test idea")
        # First stage maps to "claude_code_cli" which is missing
        assert "error" in manifest.stop_reason
