"""
Tests for FrameBufferStage.
"""

import pytest

from engine.pipeline.core import DataType, PipelineContext
from engine.pipeline.params import PipelineParams
from engine.pipeline.stages.framebuffer import FrameBufferConfig, FrameBufferStage


def make_ctx(width: int = 80, height: int = 24) -> PipelineContext:
    """Create a PipelineContext for testing."""
    ctx = PipelineContext()
    params = PipelineParams()
    params.viewport_width = width
    params.viewport_height = height
    ctx.params = params
    return ctx


class TestFrameBufferStage:
    """Tests for FrameBufferStage."""

    def test_init(self):
        """FrameBufferStage initializes with default config."""
        stage = FrameBufferStage()
        assert stage.name == "framebuffer"
        assert stage.category == "effect"
        assert stage.config.history_depth == 2

    def test_capabilities(self):
        """Stage provides framebuffer.history.{name} capability."""
        stage = FrameBufferStage()
        assert "framebuffer.history.default" in stage.capabilities

    def test_dependencies(self):
        """Stage depends on render.output."""
        stage = FrameBufferStage()
        assert "render.output" in stage.dependencies

    def test_inlet_outlet_types(self):
        """Stage accepts and produces TEXT_BUFFER."""
        stage = FrameBufferStage()
        assert DataType.TEXT_BUFFER in stage.inlet_types
        assert DataType.TEXT_BUFFER in stage.outlet_types

    def test_init_context(self):
        """init initializes context state with prefixed keys."""
        stage = FrameBufferStage()
        ctx = make_ctx()

        result = stage.init(ctx)

        assert result is True
        assert ctx.get("framebuffer.default.history") == []
        assert ctx.get("framebuffer.default.intensity_history") == []

    def test_process_stores_buffer_in_history(self):
        """process stores buffer in history."""
        stage = FrameBufferStage()
        ctx = make_ctx()
        stage.init(ctx)

        buffer = ["line1", "line2", "line3"]
        result = stage.process(buffer, ctx)

        assert result == buffer  # Pass-through
        history = ctx.get("framebuffer.default.history")
        assert len(history) == 1
        assert history[0] == buffer

    def test_process_computes_intensity(self):
        """process computes intensity map."""
        stage = FrameBufferStage()
        ctx = make_ctx()
        stage.init(ctx)

        buffer = ["hello world", "test line", ""]
        stage.process(buffer, ctx)

        intensity = ctx.get("framebuffer.default.current_intensity")
        assert intensity is not None
        assert len(intensity) == 3  # Three rows
        # Non-empty lines should have intensity > 0
        assert intensity[0] > 0
        assert intensity[1] > 0
        # Empty line should have intensity 0
        assert intensity[2] == 0.0

    def test_process_keeps_multiple_frames(self):
        """process keeps configured depth of frames."""
        config = FrameBufferConfig(history_depth=3, name="test")
        stage = FrameBufferStage(config)
        ctx = make_ctx()
        stage.init(ctx)

        # Process several frames
        for i in range(5):
            buffer = [f"frame {i}"]
            stage.process(buffer, ctx)

        history = ctx.get("framebuffer.test.history")
        assert len(history) == 3  # Only last 3 kept
        # Should be in reverse chronological order (most recent first)
        assert history[0] == ["frame 4"]
        assert history[1] == ["frame 3"]
        assert history[2] == ["frame 2"]

    def test_process_keeps_intensity_sync(self):
        """process keeps intensity history in sync with frame history."""
        config = FrameBufferConfig(history_depth=3, name="sync")
        stage = FrameBufferStage(config)
        ctx = make_ctx()
        stage.init(ctx)

        buffers = [
            ["a"],
            ["bb"],
            ["ccc"],
        ]
        for buf in buffers:
            stage.process(buf, ctx)

        prefix = "framebuffer.sync"
        frame_hist = ctx.get(f"{prefix}.history")
        intensity_hist = ctx.get(f"{prefix}.intensity_history")
        assert len(frame_hist) == len(intensity_hist) == 3

        # Each frame's intensity should match
        for i, frame in enumerate(frame_hist):
            computed_intensity = stage._compute_buffer_intensity(frame, len(frame))
            assert intensity_hist[i] == pytest.approx(computed_intensity)

    def test_get_frame(self):
        """get_frame retrieves frames from history by index."""
        config = FrameBufferConfig(history_depth=3)
        stage = FrameBufferStage(config)
        ctx = make_ctx()
        stage.init(ctx)

        buffers = [["f1"], ["f2"], ["f3"]]
        for buf in buffers:
            stage.process(buf, ctx)

        assert stage.get_frame(0, ctx) == ["f3"]  # Most recent
        assert stage.get_frame(1, ctx) == ["f2"]
        assert stage.get_frame(2, ctx) == ["f1"]
        assert stage.get_frame(3, ctx) is None  # Out of range

    def test_get_intensity(self):
        """get_intensity retrieves intensity maps by index."""
        stage = FrameBufferStage()
        ctx = make_ctx()
        stage.init(ctx)

        buffers = [["line"], ["longer line"]]
        for buf in buffers:
            stage.process(buf, ctx)

        intensity0 = stage.get_intensity(0, ctx)
        intensity1 = stage.get_intensity(1, ctx)
        assert intensity0 is not None
        assert intensity1 is not None
        # Longer line should have higher intensity (more non-space chars)
        assert sum(intensity1) > sum(intensity0)

    def test_compute_buffer_intensity_simple(self):
        """_compute_buffer_intensity computes simple density."""
        stage = FrameBufferStage()

        buf = ["abc", "  ", "de"]
        intensities = stage._compute_buffer_intensity(buf, max_rows=3)

        assert len(intensities) == 3
        # "abc" -> 3/3 = 1.0
        assert pytest.approx(intensities[0]) == 1.0
        # "  " -> 0/2 = 0.0
        assert pytest.approx(intensities[1]) == 0.0
        # "de" -> 2/2 = 1.0
        assert pytest.approx(intensities[2]) == 1.0

    def test_compute_buffer_intensity_with_ansi(self):
        """_compute_buffer_intensity strips ANSI codes."""
        stage = FrameBufferStage()

        # Line with ANSI color codes
        buf = ["\033[31mred\033[0m", "normal"]
        intensities = stage._compute_buffer_intensity(buf, max_rows=2)

        assert len(intensities) == 2
        # Should treat "red" as 3 non-space chars
        assert pytest.approx(intensities[0]) == 1.0  # "red" = 3/3
        assert pytest.approx(intensities[1]) == 1.0  # "normal" = 6/6

    def test_compute_buffer_intensity_padding(self):
        """_compute_buffer_intensity pads to max_rows."""
        stage = FrameBufferStage()

        buf = ["short"]
        intensities = stage._compute_buffer_intensity(buf, max_rows=5)

        assert len(intensities) == 5
        assert intensities[0] > 0
        assert all(i == 0.0 for i in intensities[1:])

    def test_thread_safety(self):
        """process is thread-safe."""
        from threading import Thread

        stage = FrameBufferStage(name="threadtest")
        ctx = make_ctx()
        stage.init(ctx)

        results = []

        def worker(idx):
            buffer = [f"thread {idx}"]
            stage.process(buffer, ctx)
            results.append(len(ctx.get("framebuffer.threadtest.history", [])))

        threads = [Thread(target=worker, args=(i,)) for i in range(10)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        # All threads should see consistent state
        assert len(ctx.get("framebuffer.threadtest.history")) <= 2  # Depth limit
        # All worker threads should have completed without errors
        assert len(results) == 10

    def test_cleanup(self):
        """cleanup does nothing but can be called."""
        stage = FrameBufferStage()
        # Should not raise
        stage.cleanup()
