"""Tests for base_engineer.py - BaseEngineer abstract class."""

from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch

import pytest

from reverse_api.base_engineer import BaseEngineer


class ConcreteEngineer(BaseEngineer):
    """Concrete implementation for testing."""

    async def analyze_and_generate(self) -> dict[str, Any] | None:
        return {"test": True}


class TestBaseEngineerInit:
    """Test BaseEngineer initialization."""

    def test_basic_init(self, tmp_path):
        """Basic initialization sets all attributes."""
        har_path = tmp_path / "test.har"
        har_path.touch()

        with patch("reverse_api.base_engineer.get_scripts_dir", return_value=tmp_path / "scripts"):
            with patch("reverse_api.base_engineer.MessageStore"):
                engineer = ConcreteEngineer(
                    run_id="test123",
                    har_path=har_path,
                    prompt="test prompt",
                    model="claude-sonnet-4-6",
                    output_dir=str(tmp_path),
                )
                assert engineer.run_id == "test123"
                assert engineer.har_path == har_path
                assert engineer.prompt == "test prompt"
                assert engineer.model == "claude-sonnet-4-6"
                assert engineer.output_mode == "client"
                assert engineer.is_fresh is False
                assert engineer.output_language == "python"

    def test_docs_mode(self, tmp_path):
        """Docs mode uses docs directory."""
        har_path = tmp_path / "test.har"
        har_path.touch()

        with patch("reverse_api.base_engineer.get_docs_dir", return_value=tmp_path / "docs") as mock_docs:
            with patch("reverse_api.base_engineer.MessageStore"):
                engineer = ConcreteEngineer(
                    run_id="test123",
                    har_path=har_path,
                    prompt="test prompt",
                    output_mode="docs",
                    output_dir=str(tmp_path),
                )
                mock_docs.assert_called_once()
                assert engineer.output_mode == "docs"

    def test_existing_client_language_preserved_for_iterative_runs(self, tmp_path):
        """Existing client language overrides the configured output language."""
        har_path = tmp_path / "test.har"
        har_path.touch()
        scripts_dir = tmp_path / "scripts"
        scripts_dir.mkdir()
        client_path = scripts_dir / "api_client.ts"
        client_path.write_text("export {};\n")

        with patch("reverse_api.base_engineer.get_scripts_dir", return_value=scripts_dir):
            with patch("reverse_api.base_engineer.MessageStore"):
                with patch("reverse_api.base_engineer.SessionManager") as mock_session_manager:
                    mock_session_manager.return_value.get_run.return_value = None
                    engineer = ConcreteEngineer(
                        run_id="test123",
                        har_path=har_path,
                        prompt="test prompt",
                        output_language="python",
                        output_dir=str(tmp_path),
                    )

        assert engineer.output_language == "typescript"
        assert engineer.existing_client_path == client_path

    def test_existing_client_language_uses_recorded_script_path(self, tmp_path):
        """Recorded script path takes precedence over config and stale files."""
        har_path = tmp_path / "test.har"
        har_path.touch()
        scripts_dir = tmp_path / "scripts"
        scripts_dir.mkdir()
        python_client = scripts_dir / "api_client.py"
        python_client.write_text("print('python')\n")
        typescript_client = scripts_dir / "api_client.ts"
        typescript_client.write_text("export {};\n")

        with patch("reverse_api.base_engineer.get_scripts_dir", return_value=scripts_dir):
            with patch("reverse_api.base_engineer.MessageStore"):
                with patch("reverse_api.base_engineer.SessionManager") as mock_session_manager:
                    mock_session_manager.return_value.get_run.return_value = {
                        "paths": {"script_path": str(typescript_client)}
                    }
                    engineer = ConcreteEngineer(
                        run_id="test123",
                        har_path=har_path,
                        prompt="test prompt",
                        output_language="python",
                        output_dir=str(tmp_path),
                    )

        assert engineer.output_language == "typescript"
        assert engineer.existing_client_path == typescript_client

    def test_existing_client_language_falls_back_to_newest_file(self, tmp_path):
        """Newest existing client wins when no recorded script path is available."""
        har_path = tmp_path / "test.har"
        har_path.touch()
        scripts_dir = tmp_path / "scripts"
        scripts_dir.mkdir()
        python_client = scripts_dir / "api_client.py"
        python_client.write_text("print('python')\n")
        typescript_client = scripts_dir / "api_client.ts"
        typescript_client.write_text("export {};\n")
        typescript_client.touch()

        with patch("reverse_api.base_engineer.get_scripts_dir", return_value=scripts_dir):
            with patch("reverse_api.base_engineer.MessageStore"):
                with patch("reverse_api.base_engineer.SessionManager") as mock_session_manager:
                    mock_session_manager.return_value.get_run.return_value = None
                    engineer = ConcreteEngineer(
                        run_id="test123",
                        har_path=har_path,
                        prompt="test prompt",
                        output_language="python",
                        output_dir=str(tmp_path),
                    )

        assert engineer.output_language == "typescript"
        assert engineer.existing_client_path == typescript_client

    def test_fresh_runs_can_switch_output_language(self, tmp_path):
        """Fresh runs ignore existing client language and honor the requested one."""
        har_path = tmp_path / "test.har"
        har_path.touch()
        scripts_dir = tmp_path / "scripts"
        scripts_dir.mkdir()
        (scripts_dir / "api_client.ts").write_text("export {};\n")

        with patch("reverse_api.base_engineer.get_scripts_dir", return_value=scripts_dir):
            with patch("reverse_api.base_engineer.MessageStore"):
                engineer = ConcreteEngineer(
                    run_id="test123",
                    har_path=har_path,
                    prompt="test prompt",
                    output_language="python",
                    is_fresh=True,
                    output_dir=str(tmp_path),
                )

        assert engineer.output_language == "python"
        assert engineer.existing_client_path is None


class TestBaseEngineerHelpers:
    """Test helper methods."""

    def _make_engineer(self, tmp_path, **kwargs):
        har_path = tmp_path / "test.har"
        har_path.touch()
        defaults = {
            "run_id": "test123",
            "har_path": har_path,
            "prompt": "test prompt",
            "output_dir": str(tmp_path),
        }
        defaults.update(kwargs)
        with patch("reverse_api.base_engineer.get_scripts_dir", return_value=tmp_path / "scripts"):
            with patch("reverse_api.base_engineer.get_docs_dir", return_value=tmp_path / "docs"):
                with patch("reverse_api.base_engineer.MessageStore"):
                    return ConcreteEngineer(**defaults)

    def test_get_output_extension_python(self, tmp_path):
        """Python extension."""
        eng = self._make_engineer(tmp_path, output_language="python")
        assert eng._get_output_extension() == ".py"

    def test_get_output_extension_javascript(self, tmp_path):
        """JavaScript extension."""
        eng = self._make_engineer(tmp_path, output_language="javascript")
        assert eng._get_output_extension() == ".js"

    def test_get_output_extension_typescript(self, tmp_path):
        """TypeScript extension."""
        eng = self._make_engineer(tmp_path, output_language="typescript")
        assert eng._get_output_extension() == ".ts"

    def test_get_output_extension_unknown(self, tmp_path):
        """Unknown language defaults to .py."""
        eng = self._make_engineer(tmp_path, output_language="rust")
        assert eng._get_output_extension() == ".py"

    def test_get_client_filename_python(self, tmp_path):
        """Client filename for Python."""
        eng = self._make_engineer(tmp_path, output_language="python")
        assert eng._get_client_filename() == "api_client.py"

    def test_get_client_filename_docs(self, tmp_path):
        """Client filename for docs mode."""
        eng = self._make_engineer(tmp_path, output_mode="docs")
        assert eng._get_client_filename() == "openapi.json"

    def test_get_run_command_python(self, tmp_path):
        """Run command for Python."""
        eng = self._make_engineer(tmp_path, output_language="python")
        assert eng._get_run_command() == "python api_client.py"

    def test_get_run_command_javascript(self, tmp_path):
        """Run command for JavaScript."""
        eng = self._make_engineer(tmp_path, output_language="javascript")
        assert eng._get_run_command() == "node api_client.js"

    def test_get_run_command_typescript(self, tmp_path):
        """Run command for TypeScript."""
        eng = self._make_engineer(tmp_path, output_language="typescript")
        assert eng._get_run_command() == "npx tsx api_client.ts"

    def test_get_run_command_unknown(self, tmp_path):
        """Unknown language defaults to Python command."""
        eng = self._make_engineer(tmp_path, output_language="rust")
        assert eng._get_run_command() == "python api_client.py"


class TestBaseEngineerBuildPrompt:
    """Test _build_analysis_prompt method."""

    def _make_engineer(self, tmp_path, **kwargs):
        har_path = tmp_path / "test.har"
        har_path.touch()
        defaults = {
            "run_id": "test123",
            "har_path": har_path,
            "prompt": "test prompt",
            "output_dir": str(tmp_path),
        }
        defaults.update(kwargs)
        with patch("reverse_api.base_engineer.get_scripts_dir", return_value=tmp_path / "scripts"):
            with patch("reverse_api.base_engineer.get_docs_dir", return_value=tmp_path / "docs"):
                with patch("reverse_api.base_engineer.MessageStore") as mock_ms:
                    mock_ms.return_value.messages_path = tmp_path / "messages" / "test.jsonl"
                    return ConcreteEngineer(**defaults)

    def test_python_prompt(self, tmp_path):
        """Python prompt includes Python-specific instructions."""
        eng = self._make_engineer(tmp_path, output_language="python")
        system_prompt, user_message = eng._build_prompts()
        assert "Python script" in system_prompt
        assert "requests" in system_prompt

    def test_javascript_prompt(self, tmp_path):
        """JavaScript prompt includes JS-specific instructions."""
        eng = self._make_engineer(tmp_path, output_language="javascript")
        system_prompt, user_message = eng._build_prompts()
        assert "JavaScript module" in system_prompt
        assert "fetch" in system_prompt

    def test_typescript_prompt(self, tmp_path):
        """TypeScript prompt includes TS-specific instructions."""
        eng = self._make_engineer(tmp_path, output_language="typescript")
        system_prompt, user_message = eng._build_prompts()
        assert "TypeScript module" in system_prompt
        assert "interfaces" in system_prompt

    def test_docs_prompt(self, tmp_path):
        """Docs mode prompt includes OpenAPI instructions."""
        eng = self._make_engineer(tmp_path, output_mode="docs")
        system_prompt, user_message = eng._build_prompts()
        assert "OpenAPI" in system_prompt

    def test_prompt_includes_har_path(self, tmp_path):
        """User message includes HAR file path."""
        eng = self._make_engineer(tmp_path)
        system_prompt, user_message = eng._build_prompts()
        assert str(eng.har_path) in user_message

    def test_prompt_includes_user_prompt(self, tmp_path):
        """User message includes user's original prompt."""
        eng = self._make_engineer(tmp_path, prompt="capture spotify api")
        system_prompt, user_message = eng._build_prompts()
        assert "capture spotify api" in user_message

    def test_prompt_includes_additional_instructions(self, tmp_path):
        """Additional instructions are in user message."""
        eng = self._make_engineer(tmp_path, additional_instructions="Focus on auth")
        system_prompt, user_message = eng._build_prompts()
        assert "Focus on auth" in user_message

    def test_prompt_includes_run_context(self, tmp_path):
        """User message includes run context (target run id, mode label)."""
        eng = self._make_engineer(tmp_path)
        system_prompt, user_message = eng._build_prompts()
        assert "Run Context" in user_message
        assert eng.run_id in user_message

    def test_prompt_includes_existing_client_guidance(self, tmp_path):
        """User message tells the agent to keep editing the existing client language."""
        har_path = tmp_path / "test.har"
        har_path.touch()
        scripts_dir = tmp_path / "scripts"
        scripts_dir.mkdir()
        client_path = scripts_dir / "api_client.js"
        client_path.write_text("export {};\n")

        with patch("reverse_api.base_engineer.get_scripts_dir", return_value=scripts_dir):
            with patch("reverse_api.base_engineer.get_docs_dir", return_value=tmp_path / "docs"):
                with patch("reverse_api.base_engineer.MessageStore") as mock_ms:
                    with patch("reverse_api.base_engineer.SessionManager") as mock_session_manager:
                        mock_ms.return_value.messages_path = tmp_path / "messages" / "test.jsonl"
                        mock_session_manager.return_value.get_run.return_value = None
                        eng = ConcreteEngineer(
                            run_id="test123",
                            har_path=har_path,
                            prompt="test prompt",
                            output_language="python",
                            output_dir=str(tmp_path),
                        )

        system_prompt, user_message = eng._build_prompts()
        assert str(client_path) in user_message
        assert "iterative edit" in user_message
        assert "JavaScript" in user_message


class TestBaseEngineerSync:
    """Test sync-related methods."""

    def _make_engineer(self, tmp_path, **kwargs):
        har_path = tmp_path / "test.har"
        har_path.touch()
        defaults = {
            "run_id": "test123",
            "har_path": har_path,
            "prompt": "test prompt",
            "output_dir": str(tmp_path),
        }
        defaults.update(kwargs)
        with patch("reverse_api.base_engineer.get_scripts_dir", return_value=tmp_path / "scripts"):
            with patch("reverse_api.base_engineer.get_docs_dir", return_value=tmp_path / "docs"):
                with patch("reverse_api.base_engineer.MessageStore"):
                    return ConcreteEngineer(**defaults)

    def test_start_sync_disabled(self, tmp_path):
        """Start sync does nothing when disabled."""
        eng = self._make_engineer(tmp_path, enable_sync=False)
        eng.start_sync()
        assert eng.sync_watcher is None

    def test_stop_sync_no_watcher(self, tmp_path):
        """Stop sync is safe with no watcher."""
        eng = self._make_engineer(tmp_path)
        eng.stop_sync()  # Should not raise

    def test_stop_sync_with_error(self, tmp_path):
        """Stop sync handles errors gracefully."""
        eng = self._make_engineer(tmp_path)
        mock_watcher = MagicMock()
        mock_watcher.stop.side_effect = Exception("stop failed")
        eng.sync_watcher = mock_watcher
        eng.stop_sync()  # Should not raise
        assert eng.sync_watcher is None

    def test_get_sync_status_no_watcher(self, tmp_path):
        """Sync status returns None with no watcher."""
        eng = self._make_engineer(tmp_path)
        assert eng.get_sync_status() is None

    def test_get_sync_status_with_watcher(self, tmp_path):
        """Sync status returns watcher status."""
        eng = self._make_engineer(tmp_path)
        mock_watcher = MagicMock()
        mock_watcher.get_status.return_value = {"active": True}
        eng.sync_watcher = mock_watcher
        status = eng.get_sync_status()
        assert status == {"active": True}

    def test_start_sync_enabled(self, tmp_path):
        """Start sync creates watcher when enabled."""
        scripts_dir = tmp_path / "scripts"
        scripts_dir.mkdir(parents=True)

        eng = self._make_engineer(tmp_path, enable_sync=True)
        eng.scripts_dir = scripts_dir

        with patch("reverse_api.base_engineer.generate_folder_name", return_value="test_project"):
            with patch("reverse_api.base_engineer.get_available_directory", return_value=tmp_path / "local" / "test_project"):
                with patch("reverse_api.base_engineer.FileSyncWatcher") as mock_watcher_cls:
                    mock_watcher = MagicMock()
                    mock_watcher_cls.return_value = mock_watcher

                    eng.start_sync()

                    assert eng.sync_watcher is mock_watcher
                    assert eng.local_scripts_dir == tmp_path / "local" / "test_project"
                    mock_watcher.start.assert_called_once()

    def test_start_sync_docs_mode(self, tmp_path):
        """Start sync uses docs directory in docs mode."""
        docs_dir = tmp_path / "docs"
        docs_dir.mkdir(parents=True)

        with patch("reverse_api.base_engineer.get_scripts_dir", return_value=tmp_path / "scripts"):
            with patch("reverse_api.base_engineer.get_docs_dir", return_value=docs_dir):
                with patch("reverse_api.base_engineer.MessageStore"):
                    har_path = tmp_path / "test.har"
                    har_path.touch()
                    eng = ConcreteEngineer(
                        run_id="test123",
                        har_path=har_path,
                        prompt="test prompt",
                        output_dir=str(tmp_path),
                        enable_sync=True,
                        output_mode="docs",
                    )

        with patch("reverse_api.base_engineer.generate_folder_name", return_value="test_docs"):
            with patch("reverse_api.base_engineer.get_available_directory", return_value=tmp_path / "local" / "test_docs"):
                with patch("reverse_api.base_engineer.FileSyncWatcher") as mock_watcher_cls:
                    mock_watcher = MagicMock()
                    mock_watcher_cls.return_value = mock_watcher
                    eng.start_sync()
                    assert eng.sync_watcher is not None
