"""
TDD: Plugin Architecture Tests.

Tests the BehaviorPlugin base class, PluginRegistry, and the
first concrete plugin (CarouselBrowsingPlugin).

Covers:
- Plugin lifecycle (register, unregister, activate, execute)
- Priority ordering
- Exclusive plugin chain-breaking
- Duplicate registration prevention
- Error isolation (plugin crashes don't cascade)
- CarouselBrowsingPlugin activation and execution
"""

from unittest.mock import MagicMock, patch

import pytest

from GramAddict.core.behaviors import (
    BehaviorContext,
    BehaviorPlugin,
    BehaviorResult,
    PluginRegistry,
)

# ── Test Plugins ──


class AlwaysActivePlugin(BehaviorPlugin):
    @property
    def name(self):
        return "always_active"

    @property
    def priority(self):
        return 50

    def can_activate(self, ctx):
        return True

    def execute(self, ctx):
        return BehaviorResult(executed=True, interactions=1)


class NeverActivePlugin(BehaviorPlugin):
    @property
    def name(self):
        return "never_active"

    @property
    def priority(self):
        return 50

    def can_activate(self, ctx):
        return False

    def execute(self, ctx):
        return BehaviorResult(executed=True)


class HighPriorityPlugin(BehaviorPlugin):
    @property
    def name(self):
        return "high_priority"

    @property
    def priority(self):
        return 100

    def can_activate(self, ctx):
        return True

    def execute(self, ctx):
        return BehaviorResult(executed=True, metadata={"order": "first"})


class LowPriorityPlugin(BehaviorPlugin):
    @property
    def name(self):
        return "low_priority"

    @property
    def priority(self):
        return 10

    def can_activate(self, ctx):
        return True

    def execute(self, ctx):
        return BehaviorResult(executed=True, metadata={"order": "last"})


class ExclusiveGuardPlugin(BehaviorPlugin):
    @property
    def name(self):
        return "ad_guard"

    @property
    def priority(self):
        return 100

    @property
    def exclusive(self):
        return True

    def can_activate(self, ctx):
        return "sponsored" in (ctx.context_xml or "")

    def execute(self, ctx):
        return BehaviorResult(executed=True, should_skip=True)


class CrashingPlugin(BehaviorPlugin):
    @property
    def name(self):
        return "crasher"

    @property
    def priority(self):
        return 50

    def can_activate(self, ctx):
        return True

    def execute(self, ctx):
        raise RuntimeError("Plugin exploded!")


# ── Fixtures ──


@pytest.fixture
def registry():
    PluginRegistry.reset()
    reg = PluginRegistry()
    yield reg
    PluginRegistry.reset()


@pytest.fixture
def ctx():
    """Minimal BehaviorContext for testing."""
    device = MagicMock()
    device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
    device.shell = MagicMock()

    configs = MagicMock()
    configs.args = MagicMock()
    configs.args.carousel_percentage = "50"
    configs.args.carousel_count = "2-4"

    session_state = MagicMock()

    return BehaviorContext(
        device=device,
        configs=configs,
        session_state=session_state,
        cognitive_stack={},
        context_xml='<hierarchy><node resource-id="com.instagram.android:id/row_feed_photo_profile_name"/></hierarchy>',
        sleep_mod=1.0,
    )


# ── Registry Tests ──


class TestPluginRegistry:
    """Registry must manage plugins correctly."""

    def test_register_adds_plugin(self, registry):
        registry.register(AlwaysActivePlugin())
        assert len(registry) == 1

    def test_duplicate_registration_ignored(self, registry):
        registry.register(AlwaysActivePlugin())
        registry.register(AlwaysActivePlugin())
        assert len(registry) == 1

    def test_unregister_removes_plugin(self, registry):
        registry.register(AlwaysActivePlugin())
        registry.unregister("always_active")
        assert len(registry) == 0

    def test_unregister_nonexistent_is_noop(self, registry):
        registry.unregister("nonexistent")
        assert len(registry) == 0

    def test_contains_check(self, registry):
        registry.register(AlwaysActivePlugin())
        assert "always_active" in registry
        assert "nonexistent" not in registry

    def test_plugins_sorted_by_priority(self, registry):
        registry.register(LowPriorityPlugin())
        registry.register(HighPriorityPlugin())

        plugins = registry.plugins
        assert plugins[0].name == "high_priority"
        assert plugins[1].name == "low_priority"

    def test_get_active_plugins_filters(self, registry, ctx):
        registry.register(AlwaysActivePlugin())
        registry.register(NeverActivePlugin())

        active = registry.get_active_plugins(ctx)
        assert len(active) == 1
        assert active[0].name == "always_active"

    def test_singleton_pattern(self):
        PluginRegistry.reset()
        r1 = PluginRegistry.get_instance()
        r2 = PluginRegistry.get_instance()
        assert r1 is r2
        PluginRegistry.reset()

    def test_reset_clears_singleton(self):
        PluginRegistry.reset()
        r1 = PluginRegistry.get_instance()
        PluginRegistry.reset()
        r2 = PluginRegistry.get_instance()
        assert r1 is not r2
        PluginRegistry.reset()


# ── Execution Tests ──


class TestPluginExecution:
    """Plugin execution lifecycle."""

    def test_execute_all_runs_active_plugins(self, registry, ctx):
        registry.register(AlwaysActivePlugin())
        results = registry.execute_all(ctx)
        assert len(results) == 1
        assert results[0].executed is True

    def test_execute_all_skips_inactive_plugins(self, registry, ctx):
        registry.register(NeverActivePlugin())
        results = registry.execute_all(ctx)
        assert len(results) == 0

    def test_execute_respects_priority_order(self, registry, ctx):
        registry.register(LowPriorityPlugin())
        registry.register(HighPriorityPlugin())

        results = registry.execute_all(ctx)
        assert len(results) == 2
        assert results[0].metadata["order"] == "first"
        assert results[1].metadata["order"] == "last"

    def test_exclusive_plugin_stops_chain(self, registry, ctx):
        ctx.context_xml = "sponsored content here"
        registry.register(ExclusiveGuardPlugin())
        registry.register(AlwaysActivePlugin())  # Lower priority

        results = registry.execute_all(ctx)
        # Only the guard should have run (it's exclusive)
        assert len(results) == 1
        assert results[0].should_skip is True

    def test_crashing_plugin_doesnt_cascade(self, registry, ctx):
        """A crashing plugin must not break other plugins."""
        registry.register(CrashingPlugin())

        # Must NOT raise
        results = registry.execute_all(ctx)
        assert len(results) == 1
        assert results[0].executed is False
        assert "error" in results[0].metadata


# ── BehaviorContext Tests ──


class TestBehaviorContext:
    """BehaviorContext construction."""

    def test_default_values(self):
        ctx = BehaviorContext(
            device=MagicMock(),
            configs=MagicMock(),
            session_state=MagicMock(),
            cognitive_stack={},
        )
        assert ctx.context_xml == ""
        assert ctx.sleep_mod == 1.0
        assert ctx.post_data is None
        assert ctx.username == ""


# ── BehaviorResult Tests ──


class TestBehaviorResult:
    """BehaviorResult defaults."""

    def test_default_result(self):
        result = BehaviorResult()
        assert result.executed is False
        assert result.should_continue is True
        assert result.should_skip is False
        assert result.interactions == 0
        assert result.metadata == {}

    def test_result_with_metadata(self):
        result = BehaviorResult(executed=True, interactions=3, metadata={"slides_viewed": 3})
        assert result.interactions == 3
        assert result.metadata["slides_viewed"] == 3


# ── CarouselBrowsingPlugin Tests ──


class TestCarouselBrowsingPlugin:
    """Concrete plugin: carousel browsing."""

    @pytest.fixture
    def carousel_ctx(self, ctx):
        """Context with carousel indicators."""
        ctx.context_xml = (
            "<hierarchy>"
            '<node resource-id="com.instagram.android:id/carousel_page_indicator"/>'
            '<node resource-id="com.instagram.android:id/row_feed_photo_profile_name"/>'
            "</hierarchy>"
        )
        return ctx

    def test_activates_on_carousel(self, carousel_ctx):
        from GramAddict.core.behaviors.carousel_browsing import CarouselBrowsingPlugin

        plugin = CarouselBrowsingPlugin()
        assert plugin.can_activate(carousel_ctx) is True

    def test_does_not_activate_without_carousel(self, ctx):
        from GramAddict.core.behaviors.carousel_browsing import CarouselBrowsingPlugin

        plugin = CarouselBrowsingPlugin()
        assert plugin.can_activate(ctx) is False

    def test_does_not_activate_with_zero_percentage(self, carousel_ctx):
        from GramAddict.core.behaviors.carousel_browsing import CarouselBrowsingPlugin

        carousel_ctx.configs.args.carousel_percentage = "0"
        plugin = CarouselBrowsingPlugin()
        assert plugin.can_activate(carousel_ctx) is False

    def test_execute_sends_swipe_commands(self, carousel_ctx):
        import random

        from GramAddict.core.behaviors.carousel_browsing import CarouselBrowsingPlugin

        random.seed(42)

        carousel_ctx.configs.args.carousel_percentage = "100"
        carousel_ctx.configs.args.carousel_count = "2-2"

        plugin = CarouselBrowsingPlugin()

        with patch("GramAddict.core.behaviors.carousel_browsing.sleep"):
            result = plugin.execute(carousel_ctx)

        assert result.executed is True
        assert result.interactions == 2
        assert result.metadata["slides_viewed"] == 2
        # Should have sent 2 swipe commands (exclude SendEventInjector detection calls)
        swipe_calls = [c for c in carousel_ctx.device.shell.call_args_list if "input swipe" in str(c)]
        assert len(swipe_calls) == 2

    def test_plugin_name_and_priority(self):
        from GramAddict.core.behaviors.carousel_browsing import CarouselBrowsingPlugin

        plugin = CarouselBrowsingPlugin()
        assert plugin.name == "carousel_browsing"
        assert plugin.priority == 20
        assert plugin.exclusive is False

    def test_execute_probabilistic_skip(self, carousel_ctx):
        """When random > carousel_pct, plugin should not execute."""
        import random

        from GramAddict.core.behaviors.carousel_browsing import CarouselBrowsingPlugin

        carousel_ctx.configs.args.carousel_percentage = "1"  # 1% chance
        plugin = CarouselBrowsingPlugin()

        # Force random to return high value
        random.seed(0)
        with patch("GramAddict.core.behaviors.carousel_browsing.sleep"):
            # Run many times — most should skip
            executed_count = 0
            for _ in range(100):
                result = plugin.execute(carousel_ctx)
                if result.executed:
                    executed_count += 1

            # With 1% chance, we expect very few executions
            assert executed_count < 20

    def test_full_registration_and_execution(self, carousel_ctx):
        """End-to-end: register plugin, execute via registry."""
        from GramAddict.core.behaviors.carousel_browsing import CarouselBrowsingPlugin

        PluginRegistry.reset()
        registry = PluginRegistry()
        registry.register(CarouselBrowsingPlugin())

        carousel_ctx.configs.args.carousel_percentage = "100"
        carousel_ctx.configs.args.carousel_count = "1-1"

        with patch("GramAddict.core.behaviors.carousel_browsing.sleep"):
            results = registry.execute_all(carousel_ctx)

        assert len(results) == 1
        assert results[0].executed is True

        PluginRegistry.reset()
