"""
GoalDecomposer TDD Tests
=========================

The GoalDecomposer takes mission config + enabled plugins and produces
a weighted list of concrete Tasks. No LLM, no device, pure logic.

This is the bridge between "what do I want?" (mission) and
"what can I do?" (plugins) → "what should I do now?" (Task).
"""


class TestGoalDecomposerGeneratesTasks:
    """Tests that GoalDecomposer produces correct tasks from plugin config."""

    def test_generates_feed_task_when_likes_enabled(self):
        """If likes plugin is enabled and feed action is configured,
        the decomposer MUST produce a HomeFeed browsing task."""
        from GramAddict.core.goal_decomposer import GoalDecomposer

        plugins = {
            "likes": {"percentage": 100, "count": "2-3"},
        }
        actions = {"feed": "5-10"}
        mission = {"strategy": "aggressive_growth"}

        decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission)
        tasks = decomposer.generate_tasks()

        feed_tasks = [t for t in tasks if t.target_screen == "HomeFeed"]
        assert len(feed_tasks) >= 1, "Must generate at least one HomeFeed task when likes + feed are enabled"
        assert feed_tasks[0].budget_posts > 0
        assert feed_tasks[0].weight > 0

    def test_generates_explore_task_when_explore_configured(self):
        from GramAddict.core.goal_decomposer import GoalDecomposer

        plugins = {"likes": {"percentage": 100}}
        actions = {"explore": "5-10"}
        mission = {"strategy": "aggressive_growth"}

        decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission)
        tasks = decomposer.generate_tasks()

        explore_tasks = [t for t in tasks if t.target_screen == "ExploreFeed"]
        assert len(explore_tasks) >= 1

    def test_generates_story_task_when_story_view_enabled(self):
        from GramAddict.core.goal_decomposer import GoalDecomposer

        plugins = {"story_view": {"percentage": 80, "count": "1-3"}}
        actions = {"feed": "5-10"}
        mission = {"strategy": "community_builder"}

        decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission)
        tasks = decomposer.generate_tasks()

        story_tasks = [t for t in tasks if t.target_screen == "StoriesFeed"]
        assert len(story_tasks) >= 1

    def test_generates_no_tasks_when_no_plugins_enabled(self):
        from GramAddict.core.goal_decomposer import GoalDecomposer

        plugins = {}
        actions = {}
        mission = {"strategy": "passive_learning"}

        decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission)
        tasks = decomposer.generate_tasks()

        assert len(tasks) == 0, "No enabled plugins = no tasks"

    def test_task_weights_reflect_aggressive_growth_strategy(self):
        """aggressive_growth should weight explore higher than home feed."""
        from GramAddict.core.goal_decomposer import GoalDecomposer

        plugins = {
            "likes": {"percentage": 100},
            "follow": {"percentage": 100},
            "story_view": {"percentage": 80},
        }
        actions = {"feed": "5-10", "explore": "5-10"}
        mission = {"strategy": "aggressive_growth"}

        decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission)
        tasks = decomposer.generate_tasks()

        explore_weight = sum(t.weight for t in tasks if t.target_screen == "ExploreFeed")
        home_weight = sum(t.weight for t in tasks if t.target_screen == "HomeFeed")

        assert (
            explore_weight > home_weight
        ), f"aggressive_growth must weight explore ({explore_weight}) > home ({home_weight})"

    def test_community_builder_weights_home_higher(self):
        from GramAddict.core.goal_decomposer import GoalDecomposer

        plugins = {
            "likes": {"percentage": 100},
            "story_view": {"percentage": 80},
        }
        actions = {"feed": "5-10", "explore": "5-10"}
        mission = {"strategy": "community_builder"}

        decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission)
        tasks = decomposer.generate_tasks()

        home_weight = sum(t.weight for t in tasks if t.target_screen == "HomeFeed")
        explore_weight = sum(t.weight for t in tasks if t.target_screen == "ExploreFeed")

        assert (
            home_weight > explore_weight
        ), f"community_builder must weight home ({home_weight}) > explore ({explore_weight})"

    def test_budget_parsed_from_range_string(self):
        from GramAddict.core.goal_decomposer import GoalDecomposer

        plugins = {"likes": {"percentage": 100}}
        actions = {"feed": "5-10"}
        mission = {"strategy": "aggressive_growth"}

        decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission)
        tasks = decomposer.generate_tasks()

        feed_task = [t for t in tasks if t.target_screen == "HomeFeed"][0]
        assert 5 <= feed_task.budget_posts <= 10

    def test_dm_task_generated_when_dm_reply_enabled(self):
        from GramAddict.core.goal_decomposer import GoalDecomposer

        plugins = {"dm_reply": {"enabled": True}}
        actions = {"feed": "5-10"}
        mission = {"strategy": "aggressive_growth"}

        decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission)
        tasks = decomposer.generate_tasks()

        dm_tasks = [t for t in tasks if t.target_screen == "MessageInbox"]
        assert len(dm_tasks) >= 1

    def test_task_has_required_fields(self):
        from GramAddict.core.goal_decomposer import GoalDecomposer, Task

        plugins = {"likes": {"percentage": 100}}
        actions = {"feed": "5-10"}
        mission = {"strategy": "aggressive_growth"}

        decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission)
        tasks = decomposer.generate_tasks()

        for task in tasks:
            assert isinstance(task, Task)
            assert task.verb, "Task must have a verb"
            assert task.target_screen, "Task must have a target_screen"
            assert task.intent, "Task must have a human-readable intent"
            assert task.weight > 0, "Task must have positive weight"

    def test_disabled_plugin_produces_no_task(self):
        """A plugin with enabled: false must not generate tasks."""
        from GramAddict.core.goal_decomposer import GoalDecomposer

        plugins = {
            "likes": {"percentage": 100},
            "dm_reply": {"enabled": False},
        }
        actions = {"feed": "5-10"}
        mission = {"strategy": "aggressive_growth"}

        decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission)
        tasks = decomposer.generate_tasks()

        dm_tasks = [t for t in tasks if t.target_screen == "MessageInbox"]
        assert len(dm_tasks) == 0, "Disabled dm_reply must NOT produce MessageInbox task"

    def test_zero_percentage_plugin_produces_no_task(self):
        """A plugin with percentage: 0 must not generate tasks."""
        from GramAddict.core.goal_decomposer import GoalDecomposer

        plugins = {"follow": {"percentage": 0}}
        actions = {"feed": "5-10"}
        mission = {"strategy": "aggressive_growth"}

        decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission)
        tasks = decomposer.generate_tasks()

        # follow with 0% should not create tasks on its own
        # but likes are not enabled either, so no tasks at all
        assert len(tasks) == 0

    def test_all_task_target_screens_are_routable(self):
        """Every target_screen a Task references must exist in ScreenTopology."""
        from GramAddict.core.goal_decomposer import GoalDecomposer
        from GramAddict.core.screen_topology import ScreenTopology

        plugins = {
            "likes": {"percentage": 100},
            "follow": {"percentage": 100},
            "comment": {"percentage": 40},
            "story_view": {"percentage": 80},
            "dm_reply": {"enabled": True},
        }
        actions = {"feed": "5-10", "explore": "5-10"}
        mission = {"strategy": "aggressive_growth"}

        decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission)
        tasks = decomposer.generate_tasks()

        for task in tasks:
            assert task.target_screen in ScreenTopology.SCREEN_NAME_MAP, (
                f"Task target_screen '{task.target_screen}' is not in ScreenTopology! "
                f"The bot cannot navigate there."
            )


class TestGoalDecomposerFromConfig:
    """Tests that GoalDecomposer correctly derives tasks from config-like dicts.
    Validates that the legacy `goals:` config is not needed."""

    def test_decomposer_produces_tasks_without_goals(self):
        """Tasks come from plugins + actions, NOT from goals list."""
        from GramAddict.core.goal_decomposer import GoalDecomposer

        plugins = {"likes": {"percentage": 100}}
        actions = {"feed": "5-10"}
        mission = {"strategy": "aggressive_growth"}

        decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission)
        tasks = decomposer.generate_tasks()

        assert len(tasks) > 0, "Tasks must come from plugins, not goals"

    def test_decomposer_accepts_empty_mission(self):
        """If no mission is provided, default to aggressive_growth."""
        from GramAddict.core.goal_decomposer import GoalDecomposer

        plugins = {"likes": {"percentage": 100}}
        actions = {"feed": "5-10"}

        decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission={})
        tasks = decomposer.generate_tasks()

        assert len(tasks) > 0, "Empty mission must fall back to aggressive_growth"


class TestGrowthBrainTaskSelection:
    """Tests that GrowthBrain.select_task() picks from concrete Task objects."""

    def test_select_task_returns_task_object(self):
        from GramAddict.core.goal_decomposer import GoalDecomposer, Task
        from GramAddict.core.growth_brain import GrowthBrain
        from GramAddict.core.dopamine_engine import DopamineEngine

        plugins = {"likes": {"percentage": 100}, "story_view": {"percentage": 80}}
        actions = {"feed": "5-10", "explore": "5-10"}
        mission = {"strategy": "aggressive_growth"}

        decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission)
        tasks = decomposer.generate_tasks()
        dopamine = DopamineEngine()

        brain = GrowthBrain(username="test", persona_interests=[])
        selected = brain.select_task(dopamine, tasks)

        assert isinstance(selected, Task), f"Expected Task, got {type(selected)}"
        assert selected in tasks

    def test_select_task_returns_none_on_empty_tasks(self):
        from GramAddict.core.growth_brain import GrowthBrain
        from GramAddict.core.dopamine_engine import DopamineEngine

        dopamine = DopamineEngine()
        brain = GrowthBrain(username="test", persona_interests=[])
        selected = brain.select_task(dopamine, [])

        assert selected is None, "Empty task list must return None"

    def test_select_task_returns_none_on_high_boredom(self):
        from GramAddict.core.goal_decomposer import GoalDecomposer
        from GramAddict.core.growth_brain import GrowthBrain
        from GramAddict.core.dopamine_engine import DopamineEngine

        plugins = {"likes": {"percentage": 100}}
        actions = {"feed": "5-10"}
        mission = {"strategy": "aggressive_growth"}

        decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission)
        tasks = decomposer.generate_tasks()
        dopamine = DopamineEngine()
        dopamine.boredom = 95.0

        brain = GrowthBrain(username="test", persona_interests=[])
        selected = brain.select_task(dopamine, tasks)

        assert selected is None, "High boredom must return None (= ShiftContext signal)"

    def test_select_task_uses_weights(self):
        """Over many selections, higher-weighted tasks should appear more often."""
        from GramAddict.core.goal_decomposer import GoalDecomposer, Task
        from GramAddict.core.growth_brain import GrowthBrain
        from GramAddict.core.dopamine_engine import DopamineEngine

        plugins = {"likes": {"percentage": 100}, "story_view": {"percentage": 80}}
        actions = {"feed": "5-10", "explore": "5-10"}
        mission = {"strategy": "aggressive_growth"}

        decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission)
        tasks = decomposer.generate_tasks()
        dopamine = DopamineEngine()
        brain = GrowthBrain(username="test", persona_interests=[])

        # Run 100 selections and count
        counts: dict = {}
        for _ in range(100):
            selected = brain.select_task(dopamine, tasks)
            if selected:
                counts[selected.target_screen] = counts.get(selected.target_screen, 0) + 1

        # With aggressive_growth, ExploreFeed (weight 0.45) should dominate
        assert len(counts) > 1, "Selection must pick from multiple screens"


class TestOrchestratorTaskRouting:
    """Tests that every Task produced by GoalDecomposer is routable by ScreenTopology."""

    def test_task_to_screen_topology_mapping(self):
        """Each Task.target_screen MUST exist in ScreenTopology.SCREEN_NAME_MAP."""
        from GramAddict.core.goal_decomposer import GoalDecomposer
        from GramAddict.core.screen_topology import ScreenTopology

        plugins = {
            "likes": {"percentage": 100},
            "follow": {"percentage": 100},
            "comment": {"percentage": 40},
            "story_view": {"percentage": 80},
            "dm_reply": {"enabled": True},
        }
        actions = {"feed": "5-10", "explore": "5-10", "reels": "3-5"}
        mission = {"strategy": "aggressive_growth"}

        decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission)
        tasks = decomposer.generate_tasks()

        assert len(tasks) > 0

        for task in tasks:
            assert task.target_screen in ScreenTopology.SCREEN_NAME_MAP, (
                f"Task '{task.verb}' → '{task.target_screen}' has no ScreenTopology mapping!"
            )

    def test_all_task_screens_reachable_from_home(self):
        """Every Task target must be reachable via BFS from HOME_FEED."""
        from GramAddict.core.goal_decomposer import GoalDecomposer
        from GramAddict.core.screen_topology import ScreenTopology
        from GramAddict.core.perception.screen_identity import ScreenType

        plugins = {
            "likes": {"percentage": 100},
            "story_view": {"percentage": 80},
            "dm_reply": {"enabled": True},
        }
        actions = {"feed": "5-10", "explore": "5-10"}
        mission = {"strategy": "community_builder"}

        decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission)
        tasks = decomposer.generate_tasks()

        for task in tasks:
            target_type = ScreenTopology.SCREEN_NAME_MAP.get(task.target_screen)
            assert target_type is not None

            if target_type == ScreenType.HOME_FEED:
                continue  # Already there

            route = ScreenTopology.find_route(ScreenType.HOME_FEED, target_type)
            assert route is not None, (
                f"No HD Map route from HOME_FEED to {task.target_screen}! "
                f"The bot cannot navigate there."
            )

    def test_task_target_screen_to_goal_string_conversion(self):
        """Every task target_screen must convert to a valid GOAP goal string."""
        from GramAddict.core.goal_decomposer import GoalDecomposer
        from GramAddict.core.screen_topology import ScreenTopology

        plugins = {"likes": {"percentage": 100}, "dm_reply": {"enabled": True}}
        actions = {"feed": "5-10", "explore": "5-10"}
        mission = {"strategy": "aggressive_growth"}

        decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission)
        tasks = decomposer.generate_tasks()

        for task in tasks:
            goal_str = ScreenTopology.screen_name_to_goal(task.target_screen)
            assert goal_str, (
                f"Task '{task.target_screen}' cannot be converted to GOAP goal!"
            )
            # The goal string should resolve back to a target screen
            target = ScreenTopology.goal_to_target_screen(goal_str)
            assert target is not None, (
                f"Goal string '{goal_str}' doesn't resolve to any ScreenType!"
            )

