"""
Tests for Glitch effect - regression tests for stability issues.
"""

import re

import pytest

from engine.display import NullDisplay
from engine.effects.types import EffectConfig, EffectContext


def strip_ansi(s: str) -> str:
    """Remove ANSI escape sequences from string."""
    return re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", s)


class TestGlitchEffectStability:
    """Regression tests for Glitch effect stability."""

    @pytest.fixture
    def effect_context(self):
        """Create a consistent effect context for testing."""
        return EffectContext(
            terminal_width=80,
            terminal_height=24,
            scroll_cam=0,
            ticker_height=20,
            frame_number=0,
        )

    @pytest.fixture
    def stable_buffer(self):
        """Create a stable buffer for testing."""
        return ["line" + str(i).zfill(2) + " " * 60 for i in range(24)]

    def test_glitch_preserves_line_count(self, effect_context, stable_buffer):
        """Glitch should not change the number of lines in buffer."""
        from engine.effects.plugins.glitch import GlitchEffect

        effect = GlitchEffect()
        result = effect.process(stable_buffer, effect_context)

        assert len(result) == len(stable_buffer), (
            f"Line count changed from {len(stable_buffer)} to {len(result)}"
        )

    def test_glitch_preserves_line_lengths(self, effect_context, stable_buffer):
        """Glitch should not change individual line lengths - prevents viewport jumping.

        Note: Effects may add ANSI color codes, so we check VISIBLE length (stripped).
        """
        from engine.effects.plugins.glitch import GlitchEffect

        effect = GlitchEffect()

        # Run multiple times to catch randomness
        for _ in range(10):
            result = effect.process(stable_buffer, effect_context)
            for i, (orig, new) in enumerate(zip(stable_buffer, result, strict=False)):
                visible_new = strip_ansi(new)
                assert len(visible_new) == len(orig), (
                    f"Line {i} visible length changed from {len(orig)} to {len(visible_new)}"
                )

    def test_glitch_no_cursor_positioning(self, effect_context, stable_buffer):
        """Glitch should not use cursor positioning escape sequences.

        Regression test: Previously glitch used \\033[{row};1H which caused
        conflicts with HUD and border rendering.
        """
        from engine.effects.plugins.glitch import GlitchEffect

        effect = GlitchEffect()
        result = effect.process(stable_buffer, effect_context)

        # Check no cursor positioning in output
        cursor_pos_pattern = re.compile(r"\033\[[0-9]+;[0-9]+H")
        for i, line in enumerate(result):
            match = cursor_pos_pattern.search(line)
            assert match is None, (
                f"Line {i} contains cursor positioning: {repr(line[:50])}"
            )

    def test_glitch_output_deterministic_given_seed(
        self, effect_context, stable_buffer
    ):
        """Glitch output should be deterministic given the same random seed."""
        from engine.effects.plugins.glitch import GlitchEffect

        effect = GlitchEffect()
        effect.config = EffectConfig(enabled=True, intensity=1.0)

        # With fixed random state, should get same result
        import random

        random.seed(42)
        result1 = effect.process(stable_buffer, effect_context)

        random.seed(42)
        result2 = effect.process(stable_buffer, effect_context)

        assert result1 == result2, (
            "Glitch should be deterministic with fixed random seed"
        )


class TestEffectViewportStability:
    """Tests to catch effects that cause viewport instability."""

    def test_null_display_stable_without_effects(self):
        """NullDisplay should produce identical output without effects."""
        display = NullDisplay()
        display.init(80, 24)

        buffer = ["test line " + "x" * 60 for _ in range(24)]

        display.show(buffer)
        output1 = display._last_buffer

        display.show(buffer)
        output2 = display._last_buffer

        assert output1 == output2, (
            "NullDisplay output should be identical for identical inputs"
        )

    def test_effect_chain_preserves_dimensions(self):
        """Effect chain should preserve buffer dimensions."""
        from engine.effects.plugins.fade import FadeEffect
        from engine.effects.plugins.glitch import GlitchEffect
        from engine.effects.plugins.noise import NoiseEffect

        ctx = EffectContext(
            terminal_width=80,
            terminal_height=24,
            scroll_cam=0,
            ticker_height=20,
        )

        buffer = ["x" * 80 for _ in range(24)]
        original_len = len(buffer)
        original_widths = [len(line) for line in buffer]

        effects = [NoiseEffect(), FadeEffect(), GlitchEffect()]

        for effect in effects:
            buffer = effect.process(buffer, ctx)

            # Check dimensions preserved (check VISIBLE length, not raw)
            # Effects may add ANSI codes which increase raw length but not visible width
            assert len(buffer) == original_len, (
                f"{effect.name} changed line count from {original_len} to {len(buffer)}"
            )
            for i, (orig_w, new_line) in enumerate(
                zip(original_widths, buffer, strict=False)
            ):
                visible_len = len(strip_ansi(new_line))
                assert visible_len == orig_w, (
                    f"{effect.name} changed line {i} visible width from {orig_w} to {visible_len}"
                )


class TestEffectTestMatrix:
    """Effect test matrix - test each effect for stability."""

    @pytest.fixture
    def effect_names(self):
        """List of all effect names to test."""
        return ["noise", "fade", "glitch", "firehose", "border"]

    @pytest.fixture
    def stable_input_buffer(self):
        """A predictable buffer for testing."""
        return [f"row{i:02d}" + " " * 70 for i in range(24)]

    @pytest.mark.parametrize("effect_name", ["noise", "fade", "glitch"])
    def test_effect_preserves_buffer_dimensions(self, effect_name, stable_input_buffer):
        """Each effect should preserve input buffer dimensions."""
        try:
            if effect_name == "border":
                # Border is handled differently
                pytest.skip("Border handled by display")
            else:
                effect_module = __import__(
                    f"engine.effects.plugins.{effect_name}",
                    fromlist=[f"{effect_name.title()}Effect"],
                )
                effect_class = getattr(effect_module, f"{effect_name.title()}Effect")
                effect = effect_class()
        except ImportError:
            pytest.skip(f"Effect {effect_name} not available")

        ctx = EffectContext(
            terminal_width=80,
            terminal_height=24,
            scroll_cam=0,
            ticker_height=20,
        )

        result = effect.process(stable_input_buffer, ctx)

        # Check dimensions preserved (check VISIBLE length)
        assert len(result) == len(stable_input_buffer), (
            f"{effect_name} changed line count"
        )
        for i, (orig, new) in enumerate(zip(stable_input_buffer, result, strict=False)):
            visible_new = strip_ansi(new)
            assert len(visible_new) == len(orig), (
                f"{effect_name} changed line {i} visible length from {len(orig)} to {len(visible_new)}"
            )

    @pytest.mark.parametrize("effect_name", ["noise", "fade", "glitch"])
    def test_effect_no_cursor_positioning(self, effect_name, stable_input_buffer):
        """Effects should not use cursor positioning (causes display conflicts)."""
        try:
            effect_module = __import__(
                f"engine.effects.plugins.{effect_name}",
                fromlist=[f"{effect_name.title()}Effect"],
            )
            effect_class = getattr(effect_module, f"{effect_name.title()}Effect")
            effect = effect_class()
        except ImportError:
            pytest.skip(f"Effect {effect_name} not available")

        ctx = EffectContext(
            terminal_width=80,
            terminal_height=24,
            scroll_cam=0,
            ticker_height=20,
        )

        result = effect.process(stable_input_buffer, ctx)

        cursor_pos_pattern = re.compile(r"\033\[[0-9]+;[0-9]+H")
        for i, line in enumerate(result):
            match = cursor_pos_pattern.search(line)
            assert match is None, (
                f"{effect_name} uses cursor positioning on line {i}: {repr(line[:50])}"
            )
