"""Tests for R3 CLI/UX refactoring — project_config, smart_defaults, do_ctl, tools_ctl, multi_ctl."""

from __future__ import annotations

import os
import textwrap
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock, patch

import pytest
import yaml


# ─── R3.5: ProjectConfig ────────────────────────────────────


class TestProjectConfig:
    """Tests for .proxym.yaml loading."""

    def test_load_empty_dir(self, tmp_path):
        from proxym.project_config import load_project_config
        cfg = load_project_config(tmp_path)
        assert cfg.name == ""
        assert cfg.default_tool == ""
        assert cfg.model == ""
        assert cfg.timeout == 300
        assert cfg.context_files == []
        assert cfg.auto_analyze is False

    def test_load_none(self):
        from proxym.project_config import load_project_config
        cfg = load_project_config(None)
        assert cfg.default_tool == ""

    def test_load_full_config(self, tmp_path):
        from proxym.project_config import load_project_config
        (tmp_path / ".proxym.yaml").write_text(yaml.dump({
            "name": "my-proj",
            "default_tool": "claude-code",
            "model": "sonnet",
            "timeout": 120,
            "context_files": ["spec.md", "src/types.ts"],
            "auto_analyze": True,
        }))
        cfg = load_project_config(tmp_path)
        assert cfg.name == "my-proj"
        assert cfg.default_tool == "claude-code"
        assert cfg.model == "sonnet"
        assert cfg.timeout == 120
        assert cfg.context_files == ["spec.md", "src/types.ts"]
        assert cfg.auto_analyze is True
        assert cfg.has_tool is True
        assert cfg.has_model is True

    def test_load_yml_extension(self, tmp_path):
        from proxym.project_config import load_project_config
        (tmp_path / ".proxym.yml").write_text(yaml.dump({"default_tool": "aider"}))
        cfg = load_project_config(tmp_path)
        assert cfg.default_tool == "aider"

    def test_yaml_takes_precedence_over_yml(self, tmp_path):
        from proxym.project_config import load_project_config
        (tmp_path / ".proxym.yaml").write_text(yaml.dump({"default_tool": "claude-code"}))
        (tmp_path / ".proxym.yml").write_text(yaml.dump({"default_tool": "aider"}))
        cfg = load_project_config(tmp_path)
        assert cfg.default_tool == "claude-code"

    def test_load_invalid_yaml(self, tmp_path):
        from proxym.project_config import load_project_config
        (tmp_path / ".proxym.yaml").write_text(":::invalid:::")
        cfg = load_project_config(tmp_path)
        assert cfg.default_tool == ""

    def test_load_non_dict_yaml(self, tmp_path):
        from proxym.project_config import load_project_config
        (tmp_path / ".proxym.yaml").write_text("- just\n- a\n- list\n")
        cfg = load_project_config(tmp_path)
        assert cfg.default_tool == ""

    def test_partial_config(self, tmp_path):
        from proxym.project_config import load_project_config
        (tmp_path / ".proxym.yaml").write_text(yaml.dump({"model": "gpt-4o"}))
        cfg = load_project_config(tmp_path)
        assert cfg.model == "gpt-4o"
        assert cfg.default_tool == ""
        assert cfg.has_tool is False
        assert cfg.has_model is True

    def test_unknown_keys_preserved_in_raw(self, tmp_path):
        from proxym.project_config import load_project_config
        (tmp_path / ".proxym.yaml").write_text(yaml.dump({"custom_key": "value123"}))
        cfg = load_project_config(tmp_path)
        assert cfg._raw.get("custom_key") == "value123"


# ─── R3.5: smart_defaults integration ───────────────────────


class TestSmartDefaultsWithConfig:
    """Tests for resolve_tool/resolve_model with ProjectConfig."""

    def test_resolve_tool_explicit_wins(self):
        from proxym.cli.smart_defaults import resolve_tool
        from proxym.project_config import ProjectConfig
        pcfg = ProjectConfig(default_tool="aider")
        assert resolve_tool("bug", explicit="claude-code", project_config=pcfg) == "claude-code"

    def test_resolve_tool_config_over_project(self):
        from proxym.cli.smart_defaults import resolve_tool
        from proxym.project_config import ProjectConfig
        pcfg = ProjectConfig(default_tool="roo-code")
        project = {"default_tool": "aider"}
        assert resolve_tool("task", project=project, project_config=pcfg) == "roo-code"

    def test_resolve_tool_project_default(self):
        from proxym.cli.smart_defaults import resolve_tool
        project = {"default_tool": "claude-code"}
        assert resolve_tool("task", project=project) == "claude-code"

    def test_resolve_tool_type_hint_fallback(self):
        from proxym.cli.smart_defaults import resolve_tool
        assert resolve_tool("bug") == "aider"

    def test_resolve_model_explicit_wins(self):
        from proxym.cli.smart_defaults import resolve_model
        from proxym.project_config import ProjectConfig
        pcfg = ProjectConfig(model="gpt-4o")
        assert resolve_model(explicit="sonnet", project_config=pcfg) == "sonnet"

    def test_resolve_model_from_config(self):
        from proxym.cli.smart_defaults import resolve_model
        from proxym.project_config import ProjectConfig
        pcfg = ProjectConfig(model="sonnet")
        assert resolve_model(project_config=pcfg) == "sonnet"

    def test_resolve_model_empty(self):
        from proxym.cli.smart_defaults import resolve_model
        assert resolve_model() == ""

    def test_build_context_snippet(self, tmp_path):
        from proxym.cli.smart_defaults import build_context_snippet
        from proxym.project_config import ProjectConfig
        (tmp_path / "spec.md").write_text("# Spec\nBuild a thing.")
        pcfg = ProjectConfig(context_files=["spec.md"])
        snippet = build_context_snippet(pcfg, str(tmp_path))
        assert "spec.md" in snippet
        assert "Build a thing." in snippet

    def test_build_context_snippet_missing_file(self, tmp_path):
        from proxym.cli.smart_defaults import build_context_snippet
        from proxym.project_config import ProjectConfig
        pcfg = ProjectConfig(context_files=["nonexistent.md"])
        snippet = build_context_snippet(pcfg, str(tmp_path))
        assert snippet == ""

    def test_build_context_snippet_no_config(self):
        from proxym.cli.smart_defaults import build_context_snippet
        assert build_context_snippet(None) == ""


# ─── R3.2: _resolve_dispatch_args ───────────────────────────


class TestResolveDispatchArgs:
    """Tests for auto-resolving tool/dir from ticket."""

    def test_explicit_tool_and_dir(self):
        from proxym.cli._groups.tools_ctl import _resolve_dispatch_args
        client = MagicMock()
        tool, pdir = _resolve_dispatch_args(client, "TKT-1", "aider", "/my/dir")
        assert tool == "aider"
        assert pdir == "/my/dir"
        client.get.assert_not_called()

    def test_resolve_from_ticket(self):
        from proxym.cli._groups.tools_ctl import _resolve_dispatch_args
        client = MagicMock()
        client.get.return_value = MagicMock(
            status_code=200,
            json=MagicMock(return_value={
                "assigned_tool": {"tool": "claude-code"},
                "project_id": "",
            })
        )
        tool, pdir = _resolve_dispatch_args(client, "TKT-1", "", "")
        assert tool == "claude-code"

    def test_resolve_from_project(self):
        from proxym.cli._groups.tools_ctl import _resolve_dispatch_args
        ticket_resp = MagicMock(
            status_code=200,
            json=MagicMock(return_value={
                "assigned_tool": None,
                "project_id": "proj-1",
            })
        )
        proj_resp = MagicMock(
            status_code=200,
            json=MagicMock(return_value={
                "default_tool": "aider",
                "local_path": "/home/user/myproj",
            })
        )
        client = MagicMock()
        client.get.side_effect = [ticket_resp, proj_resp]
        tool, pdir = _resolve_dispatch_args(client, "TKT-1", "", "")
        assert tool == "aider"
        assert pdir == "/home/user/myproj"

    def test_ticket_not_found(self):
        from proxym.cli._groups.tools_ctl import _resolve_dispatch_args
        client = MagicMock()
        client.get.return_value = MagicMock(status_code=404)
        tool, pdir = _resolve_dispatch_args(client, "TKT-BAD", "", "")
        assert tool == ""
        assert pdir == ""


# ─── R3.1: do_cmd / run_cmd ─────────────────────────────────


class TestDoCmdResolveAll:
    """Tests for _resolve_all with project config integration."""

    def test_resolve_all_with_config(self, tmp_path):
        from proxym.cli._groups.do_ctl import _resolve_all
        # Write .proxym.yaml
        (tmp_path / ".proxym.yaml").write_text(yaml.dump({
            "default_tool": "claude-code",
            "model": "sonnet",
        }))
        # Mock client
        client = MagicMock()
        client.get.return_value = MagicMock(
            status_code=200,
            json=MagicMock(return_value=[{
                "id": "p1",
                "name": "myproj",
                "local_path": str(tmp_path),
            }])
        )
        project, ttype, tool, priority, model, pcfg = _resolve_all(
            client, "fix the crash", "myproj", None, None, None, None,
        )
        assert tool == "claude-code"  # from .proxym.yaml
        assert model == "sonnet"      # from .proxym.yaml
        assert ttype == "bug"         # detected from "fix"

    def test_resolve_all_explicit_overrides_config(self, tmp_path):
        from proxym.cli._groups.do_ctl import _resolve_all
        (tmp_path / ".proxym.yaml").write_text(yaml.dump({
            "default_tool": "aider",
            "model": "gpt-4o",
        }))
        client = MagicMock()
        client.get.return_value = MagicMock(
            status_code=200,
            json=MagicMock(return_value=[{
                "id": "p1",
                "name": "proj",
                "local_path": str(tmp_path),
            }])
        )
        _, _, tool, _, model, _ = _resolve_all(
            client, "task", "proj", "claude-code", None, None, "sonnet",
        )
        assert tool == "claude-code"  # explicit wins
        assert model == "sonnet"      # explicit wins


# ─── R3.1: _follow_job ──────────────────────────────────────


class TestFollowJob:
    """Tests for _follow_job polling and streaming."""

    def test_follow_job_done(self):
        from proxym.cli._groups.do_ctl import _follow_job
        client = MagicMock()
        client.get.return_value = MagicMock(
            status_code=200,
            json=MagicMock(return_value={
                "status": "done",
                "duration_s": 15,
                "result": {"files_changed": ["a.py"], "stdout": "ok"},
            })
        )
        with patch("proxym.cli._groups.do_ctl.time.sleep"):
            result = _follow_job(client, "j1", poll_interval=1, timeout=10)
        assert result is not None
        assert result["status"] == "done"

    def test_follow_job_failed(self):
        from proxym.cli._groups.do_ctl import _follow_job
        client = MagicMock()
        client.get.return_value = MagicMock(
            status_code=200,
            json=MagicMock(return_value={
                "status": "failed",
                "error": "something broke",
                "result": {},
            })
        )
        with patch("proxym.cli._groups.do_ctl.time.sleep"):
            result = _follow_job(client, "j1", poll_interval=1, timeout=10)
        assert result is not None
        assert result["status"] == "failed"

    def test_follow_job_timeout(self):
        from proxym.cli._groups.do_ctl import _follow_job
        client = MagicMock()
        client.get.return_value = MagicMock(
            status_code=200,
            json=MagicMock(return_value={"status": "running"})
        )
        with patch("proxym.cli._groups.do_ctl.time.sleep"):
            result = _follow_job(client, "j1", poll_interval=1, timeout=3)
        assert result is None  # timed out


# ─── R3.1: _run_analyze ─────────────────────────────────────


class TestRunAnalyze:
    """Tests for _run_analyze inline analysis."""

    def test_analyze_pass(self):
        from proxym.cli._groups.do_ctl import _run_analyze
        client = MagicMock()
        client.get.return_value = MagicMock(
            status_code=200,
            json=MagicMock(return_value={
                "last_job": {"status": "done", "success": True},
            })
        )
        assert _run_analyze(client, "TKT-1") is True

    def test_analyze_fail(self):
        from proxym.cli._groups.do_ctl import _run_analyze
        client = MagicMock()
        client.get.return_value = MagicMock(
            status_code=200,
            json=MagicMock(return_value={
                "last_job": {"status": "failed", "success": False, "error": "boom"},
            })
        )
        assert _run_analyze(client, "TKT-1") is False

    def test_analyze_no_job(self):
        from proxym.cli._groups.do_ctl import _run_analyze
        client = MagicMock()
        client.get.return_value = MagicMock(
            status_code=200,
            json=MagicMock(return_value={})
        )
        assert _run_analyze(client, "TKT-1") is False

    def test_analyze_404(self):
        from proxym.cli._groups.do_ctl import _run_analyze
        client = MagicMock()
        client.get.return_value = MagicMock(status_code=404)
        assert _run_analyze(client, "TKT-BAD") is False


# ─── R3.3: multi_ctl helpers ────────────────────────────────


class TestMultiCtlHelpers:
    """Tests for _dispatch_one and _wait_for_jobs."""

    def test_dispatch_one_success(self):
        from proxym.cli._groups.multi_ctl import _dispatch_one
        client = MagicMock()
        client.post.return_value = MagicMock(
            status_code=200,
            json=MagicMock(return_value={"job": {"id": "j42"}}),
        )
        assert _dispatch_one(client, "TKT-1", "aider") == "j42"

    def test_dispatch_one_failure(self):
        from proxym.cli._groups.multi_ctl import _dispatch_one
        client = MagicMock()
        client.post.return_value = MagicMock(
            status_code=500,
            text="Internal error",
        )
        assert _dispatch_one(client, "TKT-1", "aider") is None

    def test_wait_for_jobs_all_done(self):
        from proxym.cli._groups.multi_ctl import _wait_for_jobs
        client = MagicMock()
        client.get.return_value = MagicMock(
            status_code=200,
            json=MagicMock(return_value={"status": "done", "duration_s": 10}),
        )
        with patch("proxym.cli._groups.multi_ctl.time.sleep"):
            results = _wait_for_jobs(client, ["j1", "j2"], timeout=10, poll=1)
        assert "j1" in results
        assert "j2" in results
        assert results["j1"]["status"] == "done"


# ─── R3.6: batch YAML format ────────────────────────────────


class TestBatchYAML:
    """Tests for batch file parsing (via Click invocation)."""

    def test_batch_yaml_structure(self, tmp_path):
        """Verify the expected YAML format is parseable."""
        batch = [
            {"task": "Create calculator.py", "project": "calc", "tool": "aider", "type": "feature"},
            {"task": "Write tests", "project": "calc", "tool": "claude-code", "type": "test"},
        ]
        f = tmp_path / "batch.yaml"
        f.write_text(yaml.dump(batch))
        loaded = yaml.safe_load(f.read_text())
        assert len(loaded) == 2
        assert loaded[0]["task"] == "Create calculator.py"


# ─── CLI registration ───────────────────────────────────────


class TestCLIRegistration:
    """Ensure all new commands are registered."""

    def test_all_new_commands_present(self):
        from proxym.ctl import cli
        commands = cli.commands.keys()
        for name in ["run", "dispatch", "retry", "autofix", "compare", "batch"]:
            assert name in commands, f"'{name}' not registered"

    def test_tools_status_registered(self):
        from proxym.cli._groups.tools_ctl import tools
        assert "status" in tools.commands

    def test_tools_dispatch_has_positional_arg(self):
        from proxym.cli._groups.tools_ctl import tools_dispatch
        param_names = [p.name for p in tools_dispatch.params]
        assert "ticket" in param_names

    def test_do_cmd_has_new_options(self):
        from proxym.cli._groups.do_ctl import do_cmd
        param_names = [p.name for p in do_cmd.params]
        for opt in ["model_name", "analyze", "stream", "timeout"]:
            assert opt in param_names, f"'{opt}' missing from do_cmd"

    def test_run_cmd_has_stream_option(self):
        from proxym.ctl import run_cmd
        param_names = [p.name for p in run_cmd.params]
        assert "stream" in param_names
