"""
Tests for engine.display module.
"""

import sys
from unittest.mock import MagicMock, patch

import pytest

from engine.display import DisplayRegistry, NullDisplay, TerminalDisplay, render_border
from engine.display.backends.multi import MultiDisplay


class TestDisplayProtocol:
    """Test that display backends satisfy the Display protocol."""

    def test_terminal_display_is_display(self):
        """TerminalDisplay satisfies Display protocol."""
        display = TerminalDisplay()
        assert hasattr(display, "init")
        assert hasattr(display, "show")
        assert hasattr(display, "clear")
        assert hasattr(display, "cleanup")

    def test_null_display_is_display(self):
        """NullDisplay satisfies Display protocol."""
        display = NullDisplay()
        assert hasattr(display, "init")
        assert hasattr(display, "show")
        assert hasattr(display, "clear")
        assert hasattr(display, "cleanup")


class TestDisplayRegistry:
    """Tests for DisplayRegistry class."""

    def setup_method(self):
        """Reset registry before each test."""
        DisplayRegistry._backends = {}
        DisplayRegistry._initialized = False

    def test_register_adds_backend(self):
        """register adds a backend to the registry."""
        DisplayRegistry.register("test", TerminalDisplay)
        assert DisplayRegistry.get("test") == TerminalDisplay

    def test_register_case_insensitive(self):
        """register is case insensitive."""
        DisplayRegistry.register("TEST", TerminalDisplay)
        assert DisplayRegistry.get("test") == TerminalDisplay

    def test_get_returns_none_for_unknown(self):
        """get returns None for unknown backend."""
        assert DisplayRegistry.get("unknown") is None

    def test_list_backends_returns_all(self):
        """list_backends returns all registered backends."""
        DisplayRegistry.register("a", TerminalDisplay)
        DisplayRegistry.register("b", NullDisplay)
        backends = DisplayRegistry.list_backends()
        assert "a" in backends
        assert "b" in backends

    def test_create_returns_instance(self):
        """create returns a display instance."""
        DisplayRegistry.register("test", NullDisplay)
        display = DisplayRegistry.create("test")
        assert isinstance(display, NullDisplay)

    def test_create_returns_none_for_unknown(self):
        """create returns None for unknown backend."""
        display = DisplayRegistry.create("unknown")
        assert display is None

    def test_initialize_registers_defaults(self):
        """initialize registers default backends."""
        DisplayRegistry.initialize()
        assert DisplayRegistry.get("terminal") == TerminalDisplay
        assert DisplayRegistry.get("null") == NullDisplay
        from engine.display.backends.pygame import PygameDisplay
        from engine.display.backends.websocket import WebSocketDisplay

        assert DisplayRegistry.get("websocket") == WebSocketDisplay
        assert DisplayRegistry.get("pygame") == PygameDisplay

    def test_initialize_idempotent(self):
        """initialize can be called multiple times safely."""
        DisplayRegistry.initialize()
        DisplayRegistry._backends["custom"] = TerminalDisplay
        DisplayRegistry.initialize()
        assert "custom" in DisplayRegistry.list_backends()


class TestTerminalDisplay:
    """Tests for TerminalDisplay class."""

    def test_init_sets_dimensions(self):
        """init stores terminal dimensions."""
        display = TerminalDisplay()
        display.init(80, 24)
        assert display.width == 80
        assert display.height == 24

    def test_show_returns_none(self):
        """show returns None after writing to stdout."""
        display = TerminalDisplay()
        display.width = 80
        display.height = 24
        display.show(["line1", "line2"])

    def test_clear_does_not_error(self):
        """clear works without error."""
        display = TerminalDisplay()
        display.clear()

    def test_cleanup_does_not_error(self):
        """cleanup works without error."""
        display = TerminalDisplay()
        display.cleanup()

    def test_get_dimensions_returns_cached_value(self):
        """get_dimensions returns cached dimensions for stability."""
        import os
        from unittest.mock import patch

        # Mock terminal size to ensure deterministic dimensions
        term_size = os.terminal_size((80, 24))
        with patch("os.get_terminal_size", return_value=term_size):
            display = TerminalDisplay()
            display.init(80, 24)
            d1 = display.get_dimensions()
            assert d1 == (80, 24)

    def test_show_clears_screen_before_each_frame(self):
        """show clears previous frame to prevent visual wobble.

        Regression test: Previously show() didn't clear the screen,
        causing old content to remain and creating visual wobble.
        The fix adds \\033[H\\033[J (cursor home + erase down) before each frame.
        """
        from io import BytesIO

        display = TerminalDisplay()
        display.init(80, 24)

        buffer = ["line1", "line2", "line3"]

        fake_buffer = BytesIO()
        fake_stdout = MagicMock()
        fake_stdout.buffer = fake_buffer
        with patch.object(sys, "stdout", fake_stdout):
            display.show(buffer)

        output = fake_buffer.getvalue().decode("utf-8")
        assert output.startswith("\033[H\033[J"), (
            f"Output should start with clear sequence, got: {repr(output[:20])}"
        )

    def test_show_clears_screen_on_subsequent_frames(self):
        """show clears screen on every frame, not just the first.

        Regression test: Ensures each show() call includes the clear sequence.
        """
        from io import BytesIO

        # Use target_fps=0 to disable frame skipping in test
        display = TerminalDisplay(target_fps=0)
        display.init(80, 24)

        buffer = ["line1", "line2"]

        for i in range(3):
            fake_buffer = BytesIO()
            fake_stdout = MagicMock()
            fake_stdout.buffer = fake_buffer
            with patch.object(sys, "stdout", fake_stdout):
                display.show(buffer)

            output = fake_buffer.getvalue().decode("utf-8")
            assert output.startswith("\033[H\033[J"), (
                f"Frame {i} should start with clear sequence"
            )

    def test_get_dimensions_stable_across_rapid_calls(self):
        """get_dimensions should not fluctuate when called rapidly.

        This test catches the bug where os.get_terminal_size() returns
        inconsistent values, causing visual wobble.
        """
        display = TerminalDisplay()
        display.init(80, 24)

        # Get dimensions 10 times rapidly (simulating frame loop)
        dims = [display.get_dimensions() for _ in range(10)]

        # All should be the same - this would fail if os.get_terminal_size()
        # returns different values each call
        assert len(set(dims)) == 1, f"Dimensions should be stable, got: {set(dims)}"

    def test_show_with_border_uses_render_border(self):
        """show with border=True calls render_border with FPS."""
        from unittest.mock import MagicMock

        display = TerminalDisplay()
        display.init(80, 24)

        buffer = ["line1", "line2"]

        # Mock get_monitor to provide FPS
        mock_monitor = MagicMock()
        mock_monitor.get_stats.return_value = {
            "pipeline": {"avg_ms": 16.5},
            "frame_count": 100,
        }

        # Mock render_border to verify it's called
        with (
            patch("engine.display.get_monitor", return_value=mock_monitor),
            patch("engine.display.render_border", wraps=render_border) as mock_render,
        ):
            display.show(buffer, border=True)

            # Verify render_border was called with correct arguments
            assert mock_render.called
            args, kwargs = mock_render.call_args
            # Arguments: buffer, width, height, fps, frame_time (positional)
            assert args[0] == buffer
            assert args[1] == 80
            assert args[2] == 24
            assert args[3] == pytest.approx(60.6, rel=0.1)  # fps = 1000/16.5
            assert args[4] == pytest.approx(16.5, rel=0.1)
            assert kwargs == {}  # no keyword arguments


class TestNullDisplay:
    """Tests for NullDisplay class."""

    def test_init_stores_dimensions(self):
        """init stores dimensions."""
        display = NullDisplay()
        display.init(100, 50)
        assert display.width == 100
        assert display.height == 50

    def test_show_does_nothing(self):
        """show discards buffer without error."""
        display = NullDisplay()
        display.show(["line1", "line2", "line3"])

    def test_clear_does_nothing(self):
        """clear does nothing."""
        display = NullDisplay()
        display.clear()

    def test_cleanup_does_nothing(self):
        """cleanup does nothing."""
        display = NullDisplay()
        display.cleanup()

    def test_show_stores_last_buffer(self):
        """show stores last buffer for testing inspection."""
        display = NullDisplay()
        display.init(80, 24)

        buffer = ["line1", "line2", "line3"]
        display.show(buffer)

        assert display._last_buffer == buffer

    def test_show_tracks_last_buffer_across_calls(self):
        """show updates last_buffer on each call."""
        display = NullDisplay()
        display.init(80, 24)

        display.show(["first"])
        assert display._last_buffer == ["first"]

        display.show(["second"])
        assert display._last_buffer == ["second"]


class TestRenderBorder:
    """Tests for render_border function."""

    def test_render_border_adds_corners(self):
        """render_border adds corner characters."""
        from engine.display import render_border

        buffer = ["hello", "world"]
        result = render_border(buffer, width=10, height=5)

        assert result[0][0] in "┌┎┍"  # top-left
        assert result[0][-1] in "┐┒┓"  # top-right
        assert result[-1][0] in "└┚┖"  # bottom-left
        assert result[-1][-1] in "┘┛┙"  # bottom-right

    def test_render_border_dimensions(self):
        """render_border output matches requested dimensions."""
        from engine.display import render_border

        buffer = ["line1", "line2", "line3"]
        result = render_border(buffer, width=20, height=10)

        # Output should be exactly height lines
        assert len(result) == 10
        # Each line should be exactly width characters
        for line in result:
            assert len(line) == 20

    def test_render_border_with_fps(self):
        """render_border includes FPS in top border when provided."""
        from engine.display import render_border

        buffer = ["test"]
        result = render_border(buffer, width=20, height=5, fps=60.0)

        top_line = result[0]
        assert "FPS:60" in top_line or "FPS: 60" in top_line

    def test_render_border_with_frame_time(self):
        """render_border includes frame time in bottom border when provided."""
        from engine.display import render_border

        buffer = ["test"]
        result = render_border(buffer, width=20, height=5, frame_time=16.5)

        bottom_line = result[-1]
        assert "16.5ms" in bottom_line

    def test_render_border_crops_content_to_fit(self):
        """render_border crops content to fit within borders."""
        from engine.display import render_border

        # Buffer larger than viewport
        buffer = ["x" * 100] * 50
        result = render_border(buffer, width=20, height=10)

        # Result shrinks to fit viewport
        assert len(result) == 10
        for line in result[1:-1]:  # Skip border lines
            assert len(line) == 20

    def test_render_border_preserves_content(self):
        """render_border preserves content within borders."""
        from engine.display import render_border

        buffer = ["hello world", "test line"]
        result = render_border(buffer, width=20, height=5)

        # Content should appear in the middle rows
        content_lines = result[1:-1]
        assert any("hello world" in line for line in content_lines)

    def test_render_border_with_small_buffer(self):
        """render_border handles buffers smaller than viewport."""
        from engine.display import render_border

        buffer = ["hi"]
        result = render_border(buffer, width=20, height=10)

        # Should still produce full viewport with padding
        assert len(result) == 10
        # All lines should be full width
        for line in result:
            assert len(line) == 20


class TestMultiDisplay:
    """Tests for MultiDisplay class."""

    def test_init_stores_dimensions(self):
        """init stores dimensions and forwards to displays."""
        mock_display1 = MagicMock()
        mock_display2 = MagicMock()
        multi = MultiDisplay([mock_display1, mock_display2])

        multi.init(120, 40)

        assert multi.width == 120
        assert multi.height == 40
        mock_display1.init.assert_called_once_with(120, 40, reuse=False)
        mock_display2.init.assert_called_once_with(120, 40, reuse=False)

    def test_show_forwards_to_all_displays(self):
        """show forwards buffer to all displays."""
        mock_display1 = MagicMock()
        mock_display2 = MagicMock()
        multi = MultiDisplay([mock_display1, mock_display2])

        buffer = ["line1", "line2"]
        multi.show(buffer, border=False)

        mock_display1.show.assert_called_once_with(buffer, border=False)
        mock_display2.show.assert_called_once_with(buffer, border=False)

    def test_clear_forwards_to_all_displays(self):
        """clear forwards to all displays."""
        mock_display1 = MagicMock()
        mock_display2 = MagicMock()
        multi = MultiDisplay([mock_display1, mock_display2])

        multi.clear()

        mock_display1.clear.assert_called_once()
        mock_display2.clear.assert_called_once()

    def test_cleanup_forwards_to_all_displays(self):
        """cleanup forwards to all displays."""
        mock_display1 = MagicMock()
        mock_display2 = MagicMock()
        multi = MultiDisplay([mock_display1, mock_display2])

        multi.cleanup()

        mock_display1.cleanup.assert_called_once()
        mock_display2.cleanup.assert_called_once()

    def test_empty_displays_list(self):
        """handles empty displays list gracefully."""
        multi = MultiDisplay([])
        multi.init(80, 24)
        multi.show(["test"])
        multi.clear()
        multi.cleanup()

    def test_init_with_reuse(self):
        """init passes reuse flag to child displays."""
        mock_display = MagicMock()
        multi = MultiDisplay([mock_display])

        multi.init(80, 24, reuse=True)

        mock_display.init.assert_called_once_with(80, 24, reuse=True)
