"""
Integration tests for pipeline hot-rebuild and state preservation.

Tests:
1. Viewport size control via --viewport flag
2. NullDisplay recording and save/load functionality
3. Pipeline state preservation during hot-rebuild
"""

import json
import sys
import tempfile
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).parent.parent))

from engine.display import DisplayRegistry
from engine.display.backends.null import NullDisplay
from engine.display.backends.replay import ReplayDisplay
from engine.effects import get_registry
from engine.fetch import load_cache
from engine.pipeline import Pipeline, PipelineConfig, PipelineContext
from engine.pipeline.adapters import (
    EffectPluginStage,
    FontStage,
    ViewportFilterStage,
    create_stage_from_display,
    create_stage_from_effect,
)
from engine.pipeline.params import PipelineParams


@pytest.fixture
def viewport_dims():
    """Small viewport dimensions for testing."""
    return (40, 15)


@pytest.fixture
def items():
    """Load cached source items."""
    items = load_cache()
    if not items:
        pytest.skip("No fixture cache available")
    return items


@pytest.fixture
def null_display(viewport_dims):
    """Create a NullDisplay for testing."""
    display = DisplayRegistry.create("null")
    display.init(viewport_dims[0], viewport_dims[1])
    return display


@pytest.fixture
def pipeline_with_null_display(items, null_display):
    """Create a pipeline with NullDisplay for testing."""
    import engine.effects.plugins as effects_plugins

    effects_plugins.discover_plugins()

    width, height = null_display.width, null_display.height
    params = PipelineParams()
    params.viewport_width = width
    params.viewport_height = height

    config = PipelineConfig(
        source="fixture",
        display="null",
        camera="scroll",
        effects=["noise", "fade"],
    )

    pipeline = Pipeline(config=config, context=PipelineContext())

    from engine.camera import Camera
    from engine.data_sources.sources import ListDataSource
    from engine.pipeline.adapters import CameraClockStage, CameraStage, DataSourceStage

    list_source = ListDataSource(items, name="fixture")
    pipeline.add_stage("source", DataSourceStage(list_source, name="fixture"))

    # Add camera stages (required by ViewportFilterStage)
    camera = Camera.scroll(speed=0.3)
    camera.set_canvas_size(200, 200)
    pipeline.add_stage("camera_update", CameraClockStage(camera, name="camera-clock"))
    pipeline.add_stage("camera", CameraStage(camera, name="scroll"))

    pipeline.add_stage("viewport_filter", ViewportFilterStage(name="viewport-filter"))
    pipeline.add_stage("font", FontStage(name="font"))

    effect_registry = get_registry()
    for effect_name in config.effects:
        effect = effect_registry.get(effect_name)
        if effect:
            pipeline.add_stage(
                f"effect_{effect_name}",
                create_stage_from_effect(effect, effect_name),
            )

    pipeline.add_stage("display", create_stage_from_display(null_display, "null"))
    pipeline.build()

    if not pipeline.initialize():
        pytest.fail("Failed to initialize pipeline")

    ctx = pipeline.context
    ctx.params = params
    ctx.set("display", null_display)
    ctx.set("items", items)
    ctx.set("pipeline", pipeline)
    ctx.set("pipeline_order", pipeline.execution_order)
    ctx.set("camera_y", 0)

    yield pipeline, params, null_display

    pipeline.cleanup()
    null_display.cleanup()


class TestNullDisplayRecording:
    """Tests for NullDisplay recording functionality."""

    def test_null_display_initialization(self, viewport_dims):
        """NullDisplay initializes with correct dimensions."""
        display = NullDisplay()
        display.init(viewport_dims[0], viewport_dims[1])
        assert display.width == viewport_dims[0]
        assert display.height == viewport_dims[1]

    def test_start_stop_recording(self, null_display):
        """NullDisplay can start and stop recording."""
        assert not null_display._is_recording

        null_display.start_recording()
        assert null_display._is_recording is True

        null_display.stop_recording()
        assert null_display._is_recording is False

    def test_record_frames(self, null_display, pipeline_with_null_display):
        """NullDisplay records frames when recording is enabled."""
        pipeline, params, display = pipeline_with_null_display

        display.start_recording()
        assert len(display._recorded_frames) == 0

        for frame in range(5):
            params.frame_number = frame
            pipeline.context.params = params
            pipeline.execute([])

        assert len(display._recorded_frames) == 5

    def test_get_frames(self, null_display, pipeline_with_null_display):
        """NullDisplay.get_frames() returns recorded buffers."""
        pipeline, params, display = pipeline_with_null_display

        display.start_recording()

        for frame in range(3):
            params.frame_number = frame
            pipeline.context.params = params
            pipeline.execute([])

        frames = display.get_frames()
        assert len(frames) == 3
        assert all(isinstance(f, list) for f in frames)

    def test_clear_recording(self, null_display, pipeline_with_null_display):
        """NullDisplay.clear_recording() clears recorded frames."""
        pipeline, params, display = pipeline_with_null_display

        display.start_recording()
        for frame in range(3):
            params.frame_number = frame
            pipeline.context.params = params
            pipeline.execute([])

        assert len(display._recorded_frames) == 3

        display.clear_recording()
        assert len(display._recorded_frames) == 0

    def test_save_load_recording(self, null_display, pipeline_with_null_display):
        """NullDisplay can save and load recordings."""
        pipeline, params, display = pipeline_with_null_display

        display.start_recording()
        for frame in range(3):
            params.frame_number = frame
            pipeline.context.params = params
            pipeline.execute([])

        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            temp_path = f.name

        try:
            display.save_recording(temp_path)

            with open(temp_path) as f:
                data = json.load(f)

            assert data["version"] == 1
            assert data["display"] == "null"
            assert data["frame_count"] == 3
            assert len(data["frames"]) == 3

            display2 = NullDisplay()
            display2.load_recording(temp_path)
            assert len(display2._recorded_frames) == 3

        finally:
            Path(temp_path).unlink(missing_ok=True)


class TestReplayDisplay:
    """Tests for ReplayDisplay functionality."""

    def test_replay_display_initialization(self, viewport_dims):
        """ReplayDisplay initializes correctly."""
        display = ReplayDisplay()
        display.init(viewport_dims[0], viewport_dims[1])
        assert display.width == viewport_dims[0]
        assert display.height == viewport_dims[1]

    def test_set_and_get_frames(self):
        """ReplayDisplay can set and retrieve frames."""
        display = ReplayDisplay()
        frames = [
            {"buffer": ["line1", "line2"], "width": 40, "height": 15},
            {"buffer": ["line3", "line4"], "width": 40, "height": 15},
        ]
        display.set_frames(frames)

        frame = display.get_next_frame()
        assert frame == ["line1", "line2"]

        frame = display.get_next_frame()
        assert frame == ["line3", "line4"]

        frame = display.get_next_frame()
        assert frame is None

    def test_replay_loop_mode(self):
        """ReplayDisplay can loop playback."""
        display = ReplayDisplay()
        display.set_loop(True)
        frames = [
            {"buffer": ["frame1"], "width": 40, "height": 15},
            {"buffer": ["frame2"], "width": 40, "height": 15},
        ]
        display.set_frames(frames)

        assert display.get_next_frame() == ["frame1"]
        assert display.get_next_frame() == ["frame2"]
        assert display.get_next_frame() == ["frame1"]
        assert display.get_next_frame() == ["frame2"]

    def test_replay_seek_and_reset(self):
        """ReplayDisplay supports seek and reset."""
        display = ReplayDisplay()
        frames = [
            {"buffer": [f"frame{i}"], "width": 40, "height": 15} for i in range(5)
        ]
        display.set_frames(frames)

        display.seek(3)
        assert display.get_next_frame() == ["frame3"]

        display.reset()
        assert display.get_next_frame() == ["frame0"]


class TestPipelineHotRebuild:
    """Tests for pipeline hot-rebuild and state preservation."""

    def test_pipeline_runs_with_null_display(self, pipeline_with_null_display):
        """Pipeline executes successfully with NullDisplay."""
        pipeline, params, display = pipeline_with_null_display

        for frame in range(5):
            params.frame_number = frame
            pipeline.context.params = params
            result = pipeline.execute([])

            assert result.success
            assert display._last_buffer is not None

    def test_effect_toggle_during_execution(self, pipeline_with_null_display):
        """Effects can be toggled during pipeline execution."""
        pipeline, params, display = pipeline_with_null_display

        params.frame_number = 0
        pipeline.context.params = params
        pipeline.execute([])
        buffer1 = display._last_buffer

        fade_stage = pipeline.get_stage("effect_fade")
        assert fade_stage is not None
        assert isinstance(fade_stage, EffectPluginStage)

        fade_stage._enabled = False
        fade_stage._effect.config.enabled = False

        params.frame_number = 1
        pipeline.context.params = params
        pipeline.execute([])
        buffer2 = display._last_buffer

        assert buffer1 != buffer2

    def test_state_preservation_across_rebuild(self, pipeline_with_null_display):
        """Pipeline state is preserved across hot-rebuild events."""
        pipeline, params, display = pipeline_with_null_display

        for frame in range(5):
            params.frame_number = frame
            pipeline.context.params = params
            pipeline.execute([])

        camera_y_before = pipeline.context.get("camera_y")

        fade_stage = pipeline.get_stage("effect_fade")
        if fade_stage and isinstance(fade_stage, EffectPluginStage):
            fade_stage.set_enabled(not fade_stage.is_enabled())
            fade_stage._effect.config.enabled = fade_stage.is_enabled()

        params.frame_number = 5
        pipeline.context.params = params
        pipeline.execute([])

        pipeline.context.get("camera_y")

        assert camera_y_before is not None


class TestViewportControl:
    """Tests for viewport size control."""

    def test_viewport_dimensions_applied(self, items):
        """Viewport dimensions are correctly applied to pipeline."""
        width, height = 40, 15

        display = DisplayRegistry.create("null")
        display.init(width, height)

        params = PipelineParams()
        params.viewport_width = width
        params.viewport_height = height

        config = PipelineConfig(
            source="fixture",
            display="null",
            camera="scroll",
            effects=[],
        )

        pipeline = Pipeline(config=config, context=PipelineContext())

        from engine.camera import Camera
        from engine.data_sources.sources import ListDataSource
        from engine.pipeline.adapters import (
            CameraClockStage,
            CameraStage,
            DataSourceStage,
        )

        list_source = ListDataSource(items, name="fixture")
        pipeline.add_stage("source", DataSourceStage(list_source, name="fixture"))

        # Add camera stages (required by ViewportFilterStage)
        camera = Camera.scroll(speed=0.3)
        camera.set_canvas_size(200, 200)
        pipeline.add_stage(
            "camera_update", CameraClockStage(camera, name="camera-clock")
        )
        pipeline.add_stage("camera", CameraStage(camera, name="scroll"))

        pipeline.add_stage(
            "viewport_filter", ViewportFilterStage(name="viewport-filter")
        )
        pipeline.add_stage("font", FontStage(name="font"))
        pipeline.add_stage("display", create_stage_from_display(display, "null"))
        pipeline.build()

        assert pipeline.initialize()

        ctx = pipeline.context
        ctx.params = params
        ctx.set("display", display)
        ctx.set("items", items)
        ctx.set("pipeline", pipeline)
        ctx.set("camera_y", 0)

        result = pipeline.execute(items)

        assert result.success
        assert display._last_buffer is not None

        pipeline.cleanup()
        display.cleanup()
