# SPDX-FileCopyrightText: 2025 Weibo, Inc.
#
# SPDX-License-Identifier: Apache-2.0

import asyncio
import time
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from executor.agents.base import Agent
from shared.models.execution import ExecutionRequest
from shared.status import TaskStatus


class TestAgent:
    """Test cases for the base Agent class"""

    @pytest.fixture
    def task_data(self):
        """Sample task data for testing"""
        return ExecutionRequest(
            task_id=123,
            subtask_id=456,
            task_title="Test Task",
            subtask_title="Test Subtask",
            git_url="https://github.com/test/repo.git",
            branch_name="main",
            user={
                "user_name": "testuser",
                "git_token": "test_token",
                "git_id": "12345",
                "git_login": "testuser",
                "git_email": "test@example.com",
            },
        )

    @pytest.fixture
    def mock_emitter(self):
        """Create a mock emitter for testing"""
        emitter = MagicMock()
        emitter.in_progress = AsyncMock()
        emitter.start = AsyncMock()
        emitter.done = AsyncMock()
        emitter.error = AsyncMock()
        emitter.text_delta = AsyncMock()
        return emitter

    @pytest.fixture
    def agent(self, task_data, mock_emitter):
        """Create a test agent instance"""
        return Agent(task_data, mock_emitter)

    def test_agent_initialization(self, agent, task_data):
        """Test agent initialization with task data"""
        assert agent.task_id == task_data.task_id
        assert agent.subtask_id == task_data.subtask_id
        assert agent.task_title == task_data.task_title
        assert agent.subtask_title == task_data.subtask_title
        assert agent.execution_status == TaskStatus.INITIALIZED
        assert agent.project_path is None

    def test_get_name(self, agent):
        """Test get_name returns class name"""
        assert agent.get_name() == "Agent"

    @pytest.mark.asyncio
    async def test_pre_execute_default(self, agent):
        """Test default pre_execute returns SUCCESS with no error"""
        status, error = await agent.pre_execute()
        assert status == TaskStatus.SUCCESS
        assert error is None

    def test_execute_not_implemented(self, agent):
        """Test execute raises NotImplementedError"""
        with pytest.raises(NotImplementedError):
            agent.execute()

    def test_initialize_default(self, agent):
        """Test default initialize returns SUCCESS"""
        status = agent.initialize()
        assert status == TaskStatus.SUCCESS

    def test_report_progress(self, agent, mock_emitter):
        """Test report_progress sends callback via emitter"""
        # The agent already has mock_emitter injected via fixture
        # report_progress calls self.get_emitter().in_progress()
        agent.report_progress(
            progress=50,
            status="running",
            message="Test message",
            result={"key": "value"},
        )

        # Verify emitter.in_progress was called
        mock_emitter.in_progress.assert_called_once()

    @pytest.mark.asyncio
    async def test_report_progress_non_blocking_in_async_context(
        self, agent, mock_emitter
    ):
        """Test report_progress returns quickly when called inside an event loop."""

        async def slow_in_progress():
            await asyncio.sleep(0.2)
            return {"status": "success"}

        mock_emitter.in_progress.side_effect = slow_in_progress

        start = time.perf_counter()
        agent.report_progress(
            progress=50,
            status="running",
            message="Test message",
            result={"key": "value"},
        )
        elapsed = time.perf_counter() - start

        # report_progress should not block the caller while callback is running.
        assert elapsed < 0.1

        await asyncio.sleep(0.25)
        assert mock_emitter.in_progress.await_count == 1

    @pytest.mark.asyncio
    async def test_report_progress_cancels_previous_callback_when_newer_one_arrives(
        self, agent, mock_emitter
    ):
        """Test in-progress callback is replaced when a newer progress update arrives."""

        state = {"started": 0, "cancelled": 0, "completed": 0}

        async def slow_in_progress():
            state["started"] += 1
            try:
                await asyncio.sleep(0.3)
                state["completed"] += 1
                return {"status": "success"}
            except asyncio.CancelledError:
                state["cancelled"] += 1
                raise

        mock_emitter.in_progress.side_effect = slow_in_progress
        agent.PROGRESS_CALLBACK_TIMEOUT_SECONDS = 1.0

        agent.report_progress(
            progress=10,
            status=TaskStatus.RUNNING.value,
            message="step-1",
        )
        await asyncio.sleep(0.01)

        agent.report_progress(
            progress=11,
            status=TaskStatus.RUNNING.value,
            message="step-2",
        )
        await asyncio.sleep(0.4)

        assert mock_emitter.in_progress.await_count == 2
        assert state["started"] == 2
        assert state["cancelled"] == 1
        assert state["completed"] == 1

    @pytest.mark.asyncio
    async def test_report_progress_timeout_clears_inflight_task(
        self, agent, mock_emitter
    ):
        """Test timeout clears inflight progress task to avoid stale blocking state."""

        async def hanging_in_progress():
            await asyncio.sleep(1.0)
            return {"status": "success"}

        mock_emitter.in_progress.side_effect = hanging_in_progress
        agent.PROGRESS_CALLBACK_TIMEOUT_SECONDS = 0.05

        agent.report_progress(
            progress=20,
            status=TaskStatus.RUNNING.value,
            message="step-timeout",
        )

        await asyncio.sleep(0.2)
        assert agent._inflight_progress_task is None

    @pytest.mark.asyncio
    @patch("executor.agents.base.git_util.clone_repo")
    @patch("executor.agents.base.git_util.get_repo_name_from_url")
    @patch("os.path.exists")
    async def test_download_code_success(
        self, mock_exists, mock_get_repo_name, mock_clone, agent
    ):
        """Test successful code download"""
        mock_exists.return_value = False
        mock_get_repo_name.return_value = "repo"
        mock_clone.return_value = (True, None)

        with patch.object(agent, "setup_git_config", new_callable=AsyncMock):
            await agent.download_code()

        mock_clone.assert_called_once()
        assert agent.project_path is not None

    @pytest.mark.asyncio
    @patch("executor.agents.base.git_util.get_repo_name_from_url")
    @patch("os.path.exists")
    async def test_download_code_already_exists(
        self, mock_exists, mock_get_repo_name, agent
    ):
        """Test code download when project already exists"""
        mock_exists.return_value = True
        mock_get_repo_name.return_value = "repo"

        await agent.download_code()

        # Should not raise exception
        assert True

    @pytest.mark.asyncio
    async def test_download_code_empty_git_url(self, agent):
        """Test download_code with empty git_url"""
        agent.task_data.git_url = ""
        await agent.download_code()
        # Should not raise exception
        assert True

    @pytest.mark.asyncio
    @patch("executor.agents.base.git_util.set_git_config")
    async def test_setup_git_config(self, mock_set_config, agent):
        """Test git config setup"""
        mock_set_config.return_value = (True, None)
        user_config = agent.task_data.user
        project_path = "/test/path"

        await agent.setup_git_config(user_config, project_path)

        mock_set_config.assert_called_once_with(
            project_path, user_config["git_login"], user_config["git_email"]
        )

    @pytest.mark.asyncio
    @patch("executor.agents.base.git_util.set_git_config")
    async def test_setup_git_config_no_email(self, mock_set_config, agent):
        """Test git config setup without email"""
        mock_set_config.return_value = (True, None)
        user_config = {"git_id": "12345", "git_login": "testuser"}
        project_path = "/test/path"

        await agent.setup_git_config(user_config, project_path)

        # Should generate email from git_id and git_login
        expected_email = "12345+testuser@users.noreply.github.com"
        mock_set_config.assert_called_once_with(
            project_path, "testuser", expected_email
        )

    def test_record_error_thinking_without_capability(self, agent):
        """Test _record_error_thinking when capability is not available"""
        # Should not raise exception
        agent._record_error_thinking("Test Error", "Error message")
        assert True

    def test_record_error_thinking_with_capability(self, agent):
        """Test _record_error_thinking when capability is available"""
        agent.add_thinking_step = MagicMock()

        agent._record_error_thinking("Test Error", "Error message")

        agent.add_thinking_step.assert_called_once()


class ConcreteAgent(Agent):
    """Concrete implementation of Agent for testing handle method"""

    def execute(self):
        return TaskStatus.SUCCESS


class TestAgentHandle:
    """Test cases for Agent.handle method"""

    @pytest.fixture
    def task_data(self):
        return ExecutionRequest(
            task_id=123,
            subtask_id=456,
            task_title="Test Task",
            subtask_title="Test Subtask",
        )

    @pytest.fixture
    def mock_emitter(self):
        """Create a mock emitter for testing"""
        emitter = MagicMock()
        emitter.in_progress = AsyncMock()
        emitter.start = AsyncMock()
        emitter.done = AsyncMock()
        emitter.error = AsyncMock()
        emitter.text_delta = AsyncMock()
        return emitter

    @pytest.fixture
    def concrete_agent(self, task_data, mock_emitter):
        return ConcreteAgent(task_data, mock_emitter)

    @pytest.mark.asyncio
    async def test_handle_success(self, concrete_agent):
        """Test successful handle execution"""
        status, error = await concrete_agent.handle()

        assert status == TaskStatus.SUCCESS
        assert error is None

    @pytest.mark.asyncio
    async def test_handle_with_pre_executed_status(self, concrete_agent):
        """Test handle with pre_executed parameter"""
        status, error = await concrete_agent.handle(
            pre_executed=TaskStatus.PRE_EXECUTED
        )

        assert status == TaskStatus.SUCCESS
        assert error is None
        assert concrete_agent.execution_status == TaskStatus.RUNNING

    @pytest.mark.asyncio
    async def test_handle_pre_execute_failure(self, concrete_agent):
        """Test handle when pre_execute fails"""
        with patch.object(
            concrete_agent, "pre_execute", new=AsyncMock(return_value=TaskStatus.FAILED)
        ):
            status, error = await concrete_agent.handle()

        assert status == TaskStatus.FAILED
        assert error is not None

    @pytest.mark.asyncio
    async def test_handle_execute_exception(self, concrete_agent):
        """Test handle when execute raises exception"""
        with patch.object(
            concrete_agent, "execute", side_effect=Exception("Test error")
        ):
            status, error = await concrete_agent.handle(
                pre_executed=TaskStatus.PRE_EXECUTED
            )

        assert status == TaskStatus.FAILED
        assert error is not None
        assert "Test error" in error
