"""Tests for tool adapters — build_args and get_adapter."""

import pytest
from unittest.mock import patch, MagicMock

from proxym.tools import ToolDefinition, ToolCategory, ToolCapability, ToolRegistry
from proxym.tools.adapters import (
    AiderAdapter,
    ClaudeCodeAdapter,
    CodexAdapter,
    GooseAdapter,
    InterpreterAdapter,
    LLMCliAdapter,
    PlandexAdapter,
    BaseAdapter,
    ShellAdapter,
    ToolResult,
    get_adapter,
    register_adapter,
    _ADAPTER_MAP,
)
from proxym.tools.adapters._base import _resolve_model_for_tool, _build_proxy_env
from proxym.tools.adapters._llm_fallback import _extract_file_blocks, _write_file_blocks


@pytest.fixture(autouse=True)
def reset_registry():
    ToolRegistry._instance = None
    yield
    ToolRegistry._instance = None


# ── ToolResult ────────────────────────────────────────────

class TestToolResult:
    def test_success_on_zero_exit(self):
        r = ToolResult(tool="aider", ticket_id="T-1", exit_code=0)
        assert r.success is True

    def test_failure_on_nonzero_exit(self):
        r = ToolResult(tool="aider", ticket_id="T-1", exit_code=1, stderr="err")
        assert r.success is False

    def test_summary(self):
        r = ToolResult(
            tool="aider", ticket_id="TKT-1",
            exit_code=0, stdout="line1\nline2", stderr="",
            duration_s=2.345, files_changed=["a.py"],
            execution_metadata={"adapter": "shell", "binary": "aider"},
        )
        s = r.summary()
        assert s["tool"] == "aider"
        assert s["success"] is True
        assert s["duration_s"] == 2.35
        assert s["stdout_lines"] == 2
        assert s["files_changed"] == ["a.py"]
        assert s["execution"]["adapter"] == "shell"
        assert s["execution_metadata"]["binary"] == "aider"


# ── AiderAdapter ──────────────────────────────────────────

class TestAiderAdapter:
    def _make(self):
        tool = ToolRegistry.get().get_tool("aider")
        return AiderAdapter(tool)

    def test_basic_args(self):
        a = self._make()
        args = a.build_args("fix the bug", "/tmp/proj")
        assert args[0] == "aider"
        assert "--yes" in args
        assert "--no-auto-commits" in args
        assert "--message" in args
        assert "fix the bug" in args

    def test_architect_mode(self):
        a = self._make()
        args = a.build_args("design API", "/tmp", mode="architect")
        assert "--architect" in args

    def test_model_override(self):
        a = self._make()
        args = a.build_args("x", "/tmp", model="claude-sonnet-4-20250514")
        assert "--model" in args
        idx = args.index("--model")
        assert args[idx + 1] == "claude-sonnet-4-20250514"

    def test_files_passed(self):
        a = self._make()
        args = a.build_args("fix", "/tmp", files=["src/main.py", "src/util.py"])
        assert "src/main.py" in args
        assert "src/util.py" in args


# ── ClaudeCodeAdapter ─────────────────────────────────────

class TestClaudeCodeAdapter:
    def _make(self):
        tool = ToolRegistry.get().get_tool("claude-code")
        return ClaudeCodeAdapter(tool)

    def test_basic_args(self):
        a = self._make()
        args = a.build_args("implement feature", "/tmp/proj")
        assert args[0] == "claude"
        assert "--print" in args
        assert "--dangerously-skip-permissions" in args
        assert "implement feature" in args

    def test_model_override(self):
        a = self._make()
        args = a.build_args("x", "/tmp", model="opus")
        assert "--model" in args
        idx = args.index("--model")
        assert args[idx + 1] == "opus"


# ── get_adapter / register_adapter ────────────────────────

class TestGetAdapter:
    def test_returns_aider_adapter(self):
        adapter = get_adapter("aider")
        assert adapter is not None
        assert isinstance(adapter, AiderAdapter)

    def test_returns_claude_adapter(self):
        adapter = get_adapter("claude-code")
        assert adapter is not None
        assert isinstance(adapter, ClaudeCodeAdapter)

    def test_returns_none_for_unknown(self):
        assert get_adapter("nonexistent-tool-xyz") is None

    def test_returns_none_for_tool_without_adapter(self):
        # browser has no adapter
        assert isinstance(get_adapter("browser"), ShellAdapter)

    def test_register_custom_adapter(self):
        class MyAdapter(BaseAdapter):
            def build_args(self, prompt, project_dir, mode="", model="", files=None):
                return ["echo", prompt]

        tool = ToolDefinition(name="my-custom", category=ToolCategory.CUSTOM, binary="echo")
        ToolRegistry.get().register(tool)
        register_adapter("my-custom", MyAdapter)

        adapter = get_adapter("my-custom")
        assert adapter is not None
        assert isinstance(adapter, MyAdapter)
        assert adapter.build_args("hello", "/tmp") == ["echo", "hello"]

        # Cleanup
        ToolRegistry.get().unregister("my-custom")
        _ADAPTER_MAP.pop("my-custom", None)


# ── BaseAdapter.run — binary not found ────────────────────

class TestBaseAdapterRun:
    @pytest.mark.asyncio
    async def test_run_binary_not_found_uses_llm_fallback(self):
        """When binary not found, adapter uses LLM fallback instead of returning error."""
        tool = ToolDefinition(
            name="missing", category=ToolCategory.AGENT_CLI,
            binary="nonexistent-binary-xyz-12345",
        )
        adapter = AiderAdapter(tool)
        
        # Mock litellm module to avoid real API calls
        from unittest.mock import patch, AsyncMock, MagicMock
        mock_litellm = MagicMock()
        mock_resp = AsyncMock()
        mock_resp.choices = [AsyncMock(message=AsyncMock(content="Mocked LLM response"))]
        mock_resp.usage = AsyncMock(prompt_tokens=10, completion_tokens=5)
        mock_resp.model = "openrouter/anthropic/claude-sonnet-4-20250514"
        mock_litellm.acompletion = AsyncMock(return_value=mock_resp)
        
        with patch.dict("sys.modules", {"litellm": mock_litellm}):
            result = await adapter.run("test", "/tmp", ticket_id="T-1")
        
        # LLM fallback returns exit_code 0 with LLM response
        assert result.exit_code == 0
        assert "LLM Execution Log" in result.stdout
        assert "Mocked LLM response" in result.stdout


# ── New CLI tool adapters ─────────────────────────────────

class TestCodexAdapter:
    def _make(self):
        tool = ToolRegistry.get().get_tool("codex")
        return CodexAdapter(tool)

    def test_basic_args(self):
        a = self._make()
        args = a.build_args("implement feature", "/tmp/proj")
        assert args[0] == "codex"
        assert "--full-auto" in args
        assert "implement feature" in args

    def test_model_override(self):
        a = self._make()
        args = a.build_args("x", "/tmp", model="gpt-4o")
        assert "--model" in args
        idx = args.index("--model")
        assert args[idx + 1] == "gpt-4o"


class TestGooseAdapter:
    def _make(self):
        tool = ToolRegistry.get().get_tool("goose")
        return GooseAdapter(tool)

    def test_basic_args(self):
        a = self._make()
        args = a.build_args("fix the bug", "/tmp/proj")
        assert args[0] == "goose"
        assert "run" in args
        assert "--text" in args
        assert "fix the bug" in args

    def test_model_override(self):
        a = self._make()
        args = a.build_args("x", "/tmp", model="claude-sonnet")
        assert "--model" in args
        idx = args.index("--model")
        assert args[idx + 1] == "claude-sonnet"


class TestInterpreterAdapter:
    def _make(self):
        tool = ToolRegistry.get().get_tool("open-interpreter")
        return InterpreterAdapter(tool)

    def test_basic_args(self):
        a = self._make()
        args = a.build_args("run tests", "/tmp/proj")
        assert args[0] == "interpreter"
        assert "--yes" in args
        assert "--message" in args
        assert "run tests" in args

    def test_model_override(self):
        a = self._make()
        args = a.build_args("x", "/tmp", model="openai/llama3.2")
        assert "--model" in args
        idx = args.index("--model")
        assert args[idx + 1] == "openai/llama3.2"


class TestPlandexAdapter:
    def _make(self):
        tool = ToolRegistry.get().get_tool("plandex")
        return PlandexAdapter(tool)

    def test_basic_args(self):
        a = self._make()
        args = a.build_args("build REST API", "/tmp/proj")
        assert args[0] == "plandex"
        assert "tell" in args
        assert "build REST API" in args


class TestLLMCliAdapter:
    def _make(self):
        tool = ToolRegistry.get().get_tool("llm-cli")
        return LLMCliAdapter(tool)

    def test_basic_args(self):
        a = self._make()
        args = a.build_args("explain CQRS", "/tmp/proj")
        assert args[0] == "llm"
        assert "explain CQRS" in args

    def test_model_override(self):
        a = self._make()
        args = a.build_args("x", "/tmp", model="openai/claude-sonnet")
        assert "-m" in args
        idx = args.index("-m")
        assert args[idx + 1] == "openai/claude-sonnet"


# ── New tool adapters: get_adapter ────────────────────────

class TestGetNewAdapters:
    def test_codex(self):
        adapter = get_adapter("codex")
        assert adapter is not None
        assert isinstance(adapter, CodexAdapter)

    def test_goose(self):
        adapter = get_adapter("goose")
        assert adapter is not None
        assert isinstance(adapter, GooseAdapter)

    def test_open_interpreter(self):
        adapter = get_adapter("open-interpreter")
        assert adapter is not None
        assert isinstance(adapter, InterpreterAdapter)

    def test_plandex(self):
        adapter = get_adapter("plandex")
        assert adapter is not None
        assert isinstance(adapter, PlandexAdapter)

    def test_llm_cli(self):
        adapter = get_adapter("llm-cli")
        assert adapter is not None
        assert isinstance(adapter, LLMCliAdapter)


# ── Model resolution ─────────────────────────────────────

class TestModelResolution:
    def test_registry_id_resolves_to_litellm_model(self):
        resolved = _resolve_model_for_tool("google/gemini-3-flash-preview", "aider")
        assert resolved == "gemini/gemini-3-flash-preview"

    def test_haiku_resolves_to_litellm_model(self):
        resolved = _resolve_model_for_tool("anthropic/haiku-4.5", "aider")
        assert "claude-haiku" in resolved

    def test_opus_resolves_to_litellm_model(self):
        resolved = _resolve_model_for_tool("anthropic/opus-4.6", "aider")
        assert "claude-opus" in resolved

    def test_deepseek_resolves(self):
        resolved = _resolve_model_for_tool("deepseek/v3", "aider")
        assert "deepseek" in resolved

    def test_explicit_model_passes_through(self):
        resolved = _resolve_model_for_tool("openrouter/google/gemini-3-flash-preview", "aider")
        assert resolved == "openrouter/google/gemini-3-flash-preview"


# ── Proxy env injection ──────────────────────────────────

class TestProxyEnv:
    def test_build_proxy_env_sets_openai_vars(self):
        env = _build_proxy_env()
        assert "OPENAI_API_BASE" in env
        assert "OPENAI_BASE_URL" in env
        assert "AIDER_OPENAI_API_BASE" in env
        assert "ANTHROPIC_BASE_URL" in env
        assert "/v1" in env["OPENAI_API_BASE"]


# ── LLM fallback file block parsing ──────────────────────

class TestExtractFileBlocks:
    def test_single_ts_file(self):
        content = '''Here is the implementation:

```typescript src/store.ts
export class TodoStore {
  constructor() {}
}
```

Done.'''
        blocks = _extract_file_blocks(content)
        assert len(blocks) == 1
        assert blocks[0][0] == "src/store.ts"
        assert "TodoStore" in blocks[0][1]

    def test_multiple_files(self):
        content = '''```typescript src/store.ts
export class Store {}
```

```typescript src/format.ts
export function format() {}
```

```typescript tests/store.test.ts
import { Store } from '../src/store';
```'''
        blocks = _extract_file_blocks(content)
        assert len(blocks) == 3
        paths = [b[0] for b in blocks]
        assert "src/store.ts" in paths
        assert "src/format.ts" in paths
        assert "tests/store.test.ts" in paths

    def test_no_blocks(self):
        blocks = _extract_file_blocks("No code here, just text.")
        assert blocks == []

    def test_rejects_block_without_path(self):
        content = '''```typescript
const x = 1;
```'''
        blocks = _extract_file_blocks(content)
        assert blocks == []

    def test_python_file(self):
        content = '''```python src/main.py
def hello():
    print("hello")
```'''
        blocks = _extract_file_blocks(content)
        assert len(blocks) == 1
        assert blocks[0][0] == "src/main.py"


class TestWriteFileBlocks:
    def test_writes_files_to_disk(self, tmp_path):
        blocks = [
            ("src/store.ts", "export class Store {}\n"),
            ("src/format.ts", "export function fmt() {}\n"),
        ]
        written = _write_file_blocks(str(tmp_path), blocks)
        assert len(written) == 2
        assert (tmp_path / "src" / "store.ts").read_text() == "export class Store {}\n"
        assert (tmp_path / "src" / "format.ts").read_text() == "export function fmt() {}\n"

    def test_rejects_absolute_path(self, tmp_path):
        blocks = [("/etc/passwd", "bad content")]
        written = _write_file_blocks(str(tmp_path), blocks)
        assert written == []

    def test_rejects_directory_traversal(self, tmp_path):
        blocks = [("../../etc/passwd", "bad content")]
        written = _write_file_blocks(str(tmp_path), blocks)
        assert written == []

    def test_creates_nested_dirs(self, tmp_path):
        blocks = [("deep/nested/dir/file.ts", "content")]
        written = _write_file_blocks(str(tmp_path), blocks)
        assert len(written) == 1
        assert (tmp_path / "deep" / "nested" / "dir" / "file.ts").exists()
