"""Tests for lichess_bot main module: game events and analysis."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any
from unittest.mock import MagicMock, PropertyMock, patch

import chess
import pytest

from python_pkg.lichess_bot.main import (
    BotContext,
    GameMeta,
    GameState,
    _collect_analysis_lines,
    _finalize_game,
    _insert_analysis_into_log,
    _log_analysis_progress,
    _process_analysis_output,
    _process_game_event,
    _run_analysis_subprocess,
    _write_pgn_to_log,
)

if TYPE_CHECKING:
    from pathlib import Path

# Type alias to make mypy happy with test event dicts
Event = dict[str, Any]


class TestProcessGameEvent:
    """Tests for _process_game_event."""

    def test_process_game_event_unhandled_type(self) -> None:
        """Test processing unhandled event type."""
        ctx = MagicMock()
        state = GameState()
        meta = GameMeta(game_id="game1", bot_version=1)
        event: Event = {"type": "chatLine", "text": "hello"}
        result = _process_game_event(event, ctx, state, meta)
        assert result is True

    def test_process_game_event_game_full(self) -> None:
        """Test processing gameFull event."""
        api = MagicMock()
        api.get_my_user_id.return_value = "mybot"
        engine = MagicMock()
        engine.max_time_sec = 5.0
        engine.choose_move_with_explanation.return_value = (
            chess.Move.from_uci("e2e4"),
            "opening",
        )
        ctx = BotContext(api=api, engine=engine, bot_version=1)
        state = GameState()
        meta = GameMeta(game_id="game1", bot_version=1)
        event: Event = {
            "type": "gameFull",
            "state": {"moves": "", "status": "started"},
            "white": {"id": "mybot"},
            "black": {"id": "opp"},
        }
        result = _process_game_event(event, ctx, state, meta)
        assert result is True

    def test_process_game_event_game_end(self) -> None:
        """Test processing game end event."""
        api = MagicMock()
        api.get_my_user_id.return_value = "mybot"
        engine = MagicMock()
        engine.max_time_sec = 5.0
        engine.choose_move_with_explanation.return_value = (None, "no moves")
        ctx = BotContext(api=api, engine=engine, bot_version=1)
        state = GameState(color="white", last_handled_len=-1)
        meta = GameMeta(game_id="game1", bot_version=1)
        event: Event = {
            "type": "gameState",
            "moves": "e2e4 e7e5",
            "status": "mate",
        }
        result = _process_game_event(event, ctx, state, meta)
        assert result is False

    def test_process_game_event_game_end_after_move(self) -> None:
        """Test game ends with status after handling move.

        This covers the case where _handle_move_if_needed returns True
        but status indicates game end.
        """
        api = MagicMock()
        api.get_my_user_id.return_value = "mybot"
        engine = MagicMock()
        engine.max_time_sec = 5.0
        engine.choose_move_with_explanation.return_value = (
            chess.Move.from_uci("d2d4"),
            "response",
        )
        ctx = BotContext(api=api, engine=engine, bot_version=1)
        # Black's turn - it's opponent's move, so we don't need to move
        state = GameState(color="black", last_handled_len=-1)
        meta = GameMeta(game_id="game1", bot_version=1)
        event: Event = {
            "type": "gameState",
            "moves": "e2e4",  # One move - now it's black's turn
            "status": "resign",  # Game ended with resign
        }
        result = _process_game_event(event, ctx, state, meta)
        assert result is False  # Game should end

    def test_process_game_event_unchanged_position(self) -> None:
        """Test processing event with unchanged position."""
        api = MagicMock()
        ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
        state = GameState(last_handled_len=2, color="white")
        meta = GameMeta(game_id="game1", bot_version=1)
        event: Event = {"type": "gameState", "moves": "e2e4 e7e5"}
        result = _process_game_event(event, ctx, state, meta)
        assert result is True

    def test_process_game_event_color_unknown(self) -> None:
        """Test processing event with unknown color."""
        api = MagicMock()
        api.get_my_user_id.return_value = "mybot"
        ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
        state = GameState(last_handled_len=-1)
        meta = GameMeta(game_id="game1", bot_version=1)
        event: Event = {"type": "gameState", "moves": "e2e4"}
        result = _process_game_event(event, ctx, state, meta)
        assert result is True
        assert state.last_handled_len == 1

    def test_process_game_event_color_unknown_on_gamefull(self) -> None:
        """Test processing gameFull event with still unknown color.

        This covers the branch where event_type is gameFull but color
        is not determined (e.g., spectator watching game).
        """
        api = MagicMock()
        # Return a user id that doesn't match either player
        api.get_my_user_id.return_value = "spectator"
        ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
        state = GameState(last_handled_len=-1)
        meta = GameMeta(game_id="game1", bot_version=1)
        event: Event = {
            "type": "gameFull",
            "state": {"moves": "e2e4", "status": "started"},
            "white": {"id": "player1"},
            "black": {"id": "player2"},
        }
        result = _process_game_event(event, ctx, state, meta)
        assert result is True
        # last_handled_len should NOT be updated for gameFull with unknown color
        assert state.last_handled_len == -1


class TestWritePgnToLog:
    """Tests for _write_pgn_to_log."""

    def test_write_pgn_to_log(self, tmp_path: Path) -> None:
        """Test writing PGN to log file."""
        log_path = tmp_path / "game.log"
        log_path.write_text("header\n")
        board = chess.Board()
        board.push_uci("e2e4")
        meta = GameMeta(
            game_id="game1",
            bot_version=1,
            site_url="https://lichess.org/game1",
            date_iso="2021.01.01",
            white_name="White",
            black_name="Black",
        )
        _write_pgn_to_log(log_path, board, meta)
        content = log_path.read_text()
        assert "PGN:" in content
        assert "e4" in content


class TestRunAnalysisSubprocess:
    """Tests for _run_analysis_subprocess."""

    def test_run_analysis_subprocess_script_not_found(self, tmp_path: Path) -> None:
        """Test analysis when script not found."""
        log_path = tmp_path / "game.log"
        with patch("python_pkg.lichess_bot.main.Path") as mock_path:
            mock_script = MagicMock()
            mock_script.is_file.return_value = False
            resolve = mock_path.return_value.resolve.return_value
            resolve.parent.parent.__truediv__.return_value.__truediv__.return_value = (
                mock_script
            )
            result = _run_analysis_subprocess("game1", log_path, 10)
        assert result is None

    def test_run_analysis_subprocess_success(self, tmp_path: Path) -> None:
        """Test successful analysis subprocess."""
        log_path = tmp_path / "game.log"
        log_path.write_text("test")

        mock_proc = MagicMock()
        mock_proc.stdout = iter(["  1 e4\n", "  2 e5\n"])
        mock_proc.stderr.read.return_value = ""
        mock_proc.wait.return_value = 0
        mock_proc.__enter__ = MagicMock(return_value=mock_proc)
        mock_proc.__exit__ = MagicMock(return_value=False)

        with (
            patch("python_pkg.lichess_bot.main.Path") as mock_path,
            patch(
                "python_pkg.lichess_bot.main.subprocess.Popen", return_value=mock_proc
            ),
        ):
            mock_script = MagicMock()
            mock_script.is_file.return_value = True
            resolve = mock_path.return_value.resolve.return_value
            resolve.parent.parent.__truediv__.return_value.__truediv__.return_value = (
                mock_script
            )
            result = _run_analysis_subprocess("game1", log_path, 2)

        assert result is not None


class TestProcessAnalysisOutput:
    """Tests for _process_analysis_output."""

    def test_process_analysis_output_success(self) -> None:
        """Test processing analysis output successfully."""
        mock_proc = MagicMock()
        mock_proc.stdout = iter(["  1 e4\n", "  2 e5\n"])
        mock_proc.stderr.read.return_value = ""
        mock_proc.wait.return_value = 0

        result = _process_analysis_output(mock_proc, "game1", 2)
        assert result is not None
        assert "e4" in result

    def test_process_analysis_output_error_exit(self) -> None:
        """Test processing analysis output with error exit."""
        mock_proc = MagicMock()
        mock_proc.stdout = iter(["output\n"])
        mock_proc.stderr.read.return_value = "error message"
        mock_proc.wait.return_value = 1

        result = _process_analysis_output(mock_proc, "game1", 1)
        assert result is not None
        assert "stderr" in result

    def test_process_analysis_output_error_exit_no_stderr(self) -> None:
        """Test processing analysis output with error exit but no stderr."""
        mock_proc = MagicMock()
        mock_proc.stdout = iter(["output\n"])
        mock_proc.stderr.read.return_value = ""
        mock_proc.wait.return_value = 1

        result = _process_analysis_output(mock_proc, "game1", 1)
        assert result is not None
        assert "stderr" not in result

    def test_process_analysis_output_none_pipes(self) -> None:
        """Test processing analysis output with None pipes."""
        mock_proc = MagicMock()
        mock_proc.stdout = None
        mock_proc.stderr = None

        with pytest.raises(RuntimeError, match="pipes unexpectedly None"):
            _process_analysis_output(mock_proc, "game1", 1)


class TestCollectAnalysisLines:
    """Tests for _collect_analysis_lines helper."""

    def test_collect_analysis_lines_empty_iterator(self) -> None:
        """Test collecting lines from empty iterator."""
        empty_iter: list[str] = []
        analyzed, lines = _collect_analysis_lines(iter(empty_iter), "game1", 10)
        assert analyzed == 0
        assert lines == []

    def test_collect_analysis_lines_with_content(self) -> None:
        """Test collecting lines from iterator with content."""
        content = ["  1 e4\n", "  2 e5\n", "not a ply line\n"]
        analyzed, lines = _collect_analysis_lines(iter(content), "game1", 3)
        assert analyzed == 2
        assert lines == content

    def test_collect_analysis_lines_full_iteration(self) -> None:
        """Test that all lines are collected."""
        content = ["line1\n", "  3 Nf3\n", "line3\n"]
        analyzed, lines = _collect_analysis_lines(iter(content), "game1", 1)
        assert analyzed == 1
        assert len(lines) == 3


class TestLogAnalysisProgress:
    """Tests for _log_analysis_progress."""

    def test_log_analysis_progress_with_total(self) -> None:
        """Test logging progress with known total."""
        with patch("python_pkg.lichess_bot.main._logger") as mock_logger:
            _log_analysis_progress("game1", 5, 10)
            mock_logger.info.assert_called_once()
            call_args = mock_logger.info.call_args[0]
            assert "50%" in call_args[0] % call_args[1:]

    def test_log_analysis_progress_zero_total(self) -> None:
        """Test logging progress with zero total."""
        with patch("python_pkg.lichess_bot.main._logger") as mock_logger:
            _log_analysis_progress("game1", 5, 0)
            mock_logger.info.assert_called_once()
            call_args = mock_logger.info.call_args[0]
            assert "unknown" in call_args[0]


class TestInsertAnalysisIntoLog:
    """Tests for _insert_analysis_into_log."""

    def test_insert_analysis_before_pgn(self, tmp_path: Path) -> None:
        """Test inserting analysis before PGN section."""
        log_path = tmp_path / "game.log"
        log_path.write_text("header\n\nPGN:\n1. e4\n")
        meta = GameMeta(
            game_id="game1",
            bot_version=1,
            date_iso="2021.01.01",
            white_name="White",
            black_name="Black",
        )
        _insert_analysis_into_log(log_path, "Analysis here", meta)
        content = log_path.read_text()
        assert "ANALYSIS:" in content
        assert content.index("ANALYSIS:") < content.index("PGN:")

    def test_insert_analysis_at_start(self, tmp_path: Path) -> None:
        """Test inserting analysis when PGN at start."""
        log_path = tmp_path / "game.log"
        log_path.write_text("PGN:\n1. e4\n")
        meta = GameMeta(game_id="game1", bot_version=1)
        _insert_analysis_into_log(log_path, "Analysis here", meta)
        content = log_path.read_text()
        assert "ANALYSIS:" in content

    def test_insert_analysis_no_pgn(self, tmp_path: Path) -> None:
        """Test inserting analysis when no PGN section."""
        log_path = tmp_path / "game.log"
        log_path.write_text("header\n")
        meta = GameMeta(game_id="game1", bot_version=1)
        _insert_analysis_into_log(log_path, "Analysis here", meta)
        content = log_path.read_text()
        assert "ANALYSIS:" in content

    def test_insert_analysis_oserror(self, tmp_path: Path) -> None:
        """Test inserting analysis with OSError."""
        log_path = tmp_path / "nonexistent" / "game.log"
        meta = GameMeta(game_id="game1", bot_version=1)
        # Should not raise, just log debug
        _insert_analysis_into_log(log_path, "Analysis", meta)


class TestFinalizeGame:
    """Tests for _finalize_game."""

    def test_finalize_game_no_log_path(self) -> None:
        """Test finalize game with no log path."""
        state = GameState(log_path=None)
        meta = GameMeta(game_id="game1", bot_version=1)
        _finalize_game(state, meta)  # Should not raise

    def test_finalize_game_write_error(self, tmp_path: Path) -> None:
        """Test finalize game with write error."""
        log_path = tmp_path / "game.log"
        log_path.write_text("header")
        state = GameState(log_path=log_path)
        meta = GameMeta(game_id="game1", bot_version=1)

        with patch(
            "python_pkg.lichess_bot.main._write_pgn_to_log",
            side_effect=OSError("error"),
        ):
            _finalize_game(state, meta)  # Should not raise

    def test_finalize_game_type_error_on_move_stack(self, tmp_path: Path) -> None:
        """Test finalize game with TypeError on move_stack."""
        log_path = tmp_path / "game.log"
        log_path.write_text("header\n")
        state = GameState(log_path=log_path)
        meta = GameMeta(game_id="game1", bot_version=1)

        mock_board = MagicMock()
        # Use PropertyMock to raise TypeError when move_stack is accessed
        type(mock_board).move_stack = PropertyMock(side_effect=TypeError())
        state.board = mock_board

        with patch("python_pkg.lichess_bot.main._write_pgn_to_log"):
            _finalize_game(state, meta)  # Should not raise

    def test_finalize_game_analysis_error(self, tmp_path: Path) -> None:
        """Test finalize game with analysis error."""
        log_path = tmp_path / "game.log"
        log_path.write_text("header\n")
        state = GameState(log_path=log_path)
        meta = GameMeta(game_id="game1", bot_version=1)

        with (
            patch("python_pkg.lichess_bot.main._write_pgn_to_log"),
            patch(
                "python_pkg.lichess_bot.main._run_analysis_subprocess",
                side_effect=OSError("error"),
            ),
        ):
            _finalize_game(state, meta)  # Should not raise
