"""
Tests for the build manager module.
"""

import pytest
import os
import threading
import time
import tempfile
from unittest.mock import Mock, patch, MagicMock
from esppal.build_manager import DaemonBuildManager, BuildState, BuildResult
from esppal.buffer import RingBuffer
from esppal.serial_manager import SerialManager, PortState


@pytest.fixture
def buffer():
    """Create a test buffer."""
    return RingBuffer(max_size_bytes=1024 * 1024)


@pytest.fixture
def mock_serial_manager():
    """Create a mock serial manager."""
    manager = Mock(spec=SerialManager)
    manager.state = PortState.CONNECTED
    manager.release_port.return_value = True
    manager.reclaim_port.return_value = None
    manager.wait_for_device.return_value = True
    return manager


@pytest.fixture
def temp_project_dir():
    """Create a temporary project directory."""
    with tempfile.TemporaryDirectory() as tmpdir:
        yield tmpdir


class TestBuildManagerBasics:
    """Basic tests for DaemonBuildManager."""

    def test_initial_state(self, buffer, mock_serial_manager):
        """Manager starts in idle state."""
        manager = DaemonBuildManager(buffer, mock_serial_manager)
        assert manager.state == BuildState.IDLE
        assert not manager.is_busy

    def test_project_dir_property(self, buffer, mock_serial_manager, temp_project_dir):
        """Project directory can be get/set."""
        manager = DaemonBuildManager(buffer, mock_serial_manager, project_dir=temp_project_dir)
        assert manager.project_dir == temp_project_dir

        new_dir = "/tmp/other"
        manager.project_dir = new_dir
        assert manager.project_dir == new_dir


class TestBuildDetection:
    """Tests for build system detection."""

    def test_detect_platformio(self, buffer, mock_serial_manager, temp_project_dir):
        """Detects PlatformIO project."""
        # Create platformio.ini
        with open(os.path.join(temp_project_dir, "platformio.ini"), "w") as f:
            f.write("[env:esp32dev]\n")

        manager = DaemonBuildManager(buffer, mock_serial_manager, project_dir=temp_project_dir)
        cmd = manager._detect_build_command()

        assert cmd[0] == "pio"
        assert "run" in cmd

    def test_detect_platformio_with_env(self, buffer, mock_serial_manager, temp_project_dir):
        """Detects PlatformIO with environment."""
        with open(os.path.join(temp_project_dir, "platformio.ini"), "w") as f:
            f.write("[env:esp32dev]\n")

        manager = DaemonBuildManager(buffer, mock_serial_manager, project_dir=temp_project_dir)
        cmd = manager._detect_build_command(environment="esp32dev")

        assert "-e" in cmd
        assert "esp32dev" in cmd


class TestBuildExecution:
    """Tests for build execution."""

    def test_build_writes_to_buffer(self, buffer, mock_serial_manager, temp_project_dir):
        """Build output goes to buffer."""
        manager = DaemonBuildManager(buffer, mock_serial_manager, project_dir=temp_project_dir)

        # Run a simple command
        with patch.object(manager, '_detect_build_command', return_value=["echo", "test output"]):
            result = manager.build(async_=False)

        assert result is not None
        assert result.success
        assert result.exit_code == 0

        # Check buffer has output
        entries = list(buffer.iterate_from_seq(0))
        stdout_entries = [e for seq, e in entries if e.source == "stdout"]
        assert len(stdout_entries) > 0

    def test_build_inserts_markers(self, buffer, mock_serial_manager, temp_project_dir):
        """Build inserts start/complete markers."""
        manager = DaemonBuildManager(buffer, mock_serial_manager, project_dir=temp_project_dir)

        with patch.object(manager, '_detect_build_command', return_value=["true"]):
            manager.build(async_=False)

        # Check for markers
        markers = buffer.get_markers()
        assert "build_start" in markers
        assert "build_complete" in markers

    def test_build_state_transitions(self, buffer, mock_serial_manager, temp_project_dir):
        """Build state transitions correctly."""
        manager = DaemonBuildManager(buffer, mock_serial_manager, project_dir=temp_project_dir)
        states = []

        original_set_state = manager._set_state

        def track_state(state):
            states.append(state)
            original_set_state(state)

        manager._set_state = track_state

        with patch.object(manager, '_detect_build_command', return_value=["true"]):
            manager.build(async_=False)

        assert BuildState.BUILDING in states
        assert BuildState.IDLE in states

    def test_build_failure(self, buffer, mock_serial_manager, temp_project_dir):
        """Build failure is reported."""
        manager = DaemonBuildManager(buffer, mock_serial_manager, project_dir=temp_project_dir)

        with patch.object(manager, '_detect_build_command', return_value=["false"]):
            result = manager.build(async_=False)

        assert not result.success
        assert result.exit_code != 0


class TestUploadExecution:
    """Tests for upload execution."""

    def test_upload_releases_port(self, buffer, mock_serial_manager, temp_project_dir):
        """Upload releases serial port."""
        manager = DaemonBuildManager(buffer, mock_serial_manager, project_dir=temp_project_dir)

        with patch.object(manager, '_detect_upload_command', return_value=["true"]):
            manager.upload(async_=False)

        mock_serial_manager.release_port.assert_called()

    def test_upload_reclaims_port(self, buffer, mock_serial_manager, temp_project_dir):
        """Upload reclaims serial port after completion."""
        manager = DaemonBuildManager(buffer, mock_serial_manager, project_dir=temp_project_dir)

        with patch.object(manager, '_detect_upload_command', return_value=["true"]):
            manager.upload(async_=False)

        mock_serial_manager.reclaim_port.assert_called()

    def test_upload_waits_for_device(self, buffer, mock_serial_manager, temp_project_dir):
        """Upload waits for device reconnection."""
        manager = DaemonBuildManager(buffer, mock_serial_manager, project_dir=temp_project_dir)

        with patch.object(manager, '_detect_upload_command', return_value=["true"]):
            manager.upload(async_=False)

        mock_serial_manager.wait_for_device.assert_called()

    def test_upload_inserts_markers(self, buffer, mock_serial_manager, temp_project_dir):
        """Upload inserts start/complete markers."""
        manager = DaemonBuildManager(buffer, mock_serial_manager, project_dir=temp_project_dir)

        with patch.object(manager, '_detect_upload_command', return_value=["true"]):
            manager.upload(async_=False)

        markers = buffer.get_markers()
        assert "upload_start" in markers
        assert "upload_complete" in markers


class TestAsyncBuild:
    """Tests for asynchronous builds."""

    def test_async_build_returns_immediately(self, buffer, mock_serial_manager, temp_project_dir):
        """Async build returns immediately."""
        manager = DaemonBuildManager(buffer, mock_serial_manager, project_dir=temp_project_dir)

        with patch.object(manager, '_detect_build_command', return_value=["sleep", "0.5"]):
            start = time.time()
            result = manager.build(async_=True)
            elapsed = time.time() - start

        assert result is None
        assert elapsed < 0.2
        assert manager.is_busy

        # Wait for completion
        manager.wait(timeout=2.0)
        assert not manager.is_busy

    def test_async_build_callback(self, buffer, mock_serial_manager, temp_project_dir):
        """Async build calls completion callback."""
        results = []

        def on_complete(result):
            results.append(result)

        manager = DaemonBuildManager(
            buffer, mock_serial_manager,
            project_dir=temp_project_dir,
            on_complete=on_complete
        )

        with patch.object(manager, '_detect_build_command', return_value=["true"]):
            manager.build(async_=True)
            manager.wait(timeout=2.0)

        assert len(results) == 1
        assert results[0].success


class TestOutputCallback:
    """Tests for output callbacks."""

    def test_output_callback_called(self, buffer, mock_serial_manager, temp_project_dir):
        """Output callback is called for each line."""
        outputs = []

        def on_output(stream, line):
            outputs.append((stream, line))

        manager = DaemonBuildManager(
            buffer, mock_serial_manager,
            project_dir=temp_project_dir,
            on_output=on_output
        )

        with patch.object(manager, '_detect_build_command', return_value=["echo", "hello"]):
            manager.build(async_=False)

        assert len(outputs) > 0
        stdout_lines = [line for stream, line in outputs if stream == "stdout"]
        assert any("hello" in line for line in stdout_lines)


class TestConcurrency:
    """Tests for concurrent build prevention."""

    def test_concurrent_build_rejected(self, buffer, mock_serial_manager, temp_project_dir):
        """Second build is rejected while first is running."""
        manager = DaemonBuildManager(buffer, mock_serial_manager, project_dir=temp_project_dir)

        with patch.object(manager, '_detect_build_command', return_value=["sleep", "1"]):
            manager.build(async_=True)
            time.sleep(0.1)

            # Try second build
            result = manager.build(async_=True)
            assert result is None  # Rejected because busy

        manager.wait(timeout=2.0)


class TestBuildCancel:
    """Tests for build cancellation."""

    def test_cancel_build(self, buffer, mock_serial_manager, temp_project_dir):
        """Build can be cancelled."""
        manager = DaemonBuildManager(buffer, mock_serial_manager, project_dir=temp_project_dir)

        with patch.object(manager, '_detect_build_command', return_value=["sleep", "10"]):
            manager.build(async_=True)
            time.sleep(0.2)

            assert manager.is_busy
            assert manager.cancel()

            manager.wait(timeout=2.0)
            assert not manager.is_busy

    def test_cancel_when_idle(self, buffer, mock_serial_manager, temp_project_dir):
        """Cancel returns False when idle."""
        manager = DaemonBuildManager(buffer, mock_serial_manager, project_dir=temp_project_dir)
        assert not manager.cancel()


class TestRunCommand:
    """Tests for build+upload run command."""

    def test_run_builds_then_uploads(self, buffer, mock_serial_manager, temp_project_dir):
        """Run does build then upload."""
        manager = DaemonBuildManager(buffer, mock_serial_manager, project_dir=temp_project_dir)

        with patch.object(manager, '_detect_build_command', return_value=["true"]):
            with patch.object(manager, '_detect_upload_command', return_value=["true"]):
                result = manager.run(async_=False)

        assert result.success

        # Check both markers present
        markers = buffer.get_markers()
        assert "build_start" in markers
        assert "build_complete" in markers
        assert "upload_start" in markers
        assert "upload_complete" in markers

    def test_run_stops_on_build_failure(self, buffer, mock_serial_manager, temp_project_dir):
        """Run stops if build fails."""
        manager = DaemonBuildManager(buffer, mock_serial_manager, project_dir=temp_project_dir)

        with patch.object(manager, '_detect_build_command', return_value=["false"]):
            with patch.object(manager, '_detect_upload_command', return_value=["true"]):
                result = manager.run(async_=False)

        assert not result.success

        # Upload should not have run
        mock_serial_manager.release_port.assert_not_called()
