"""Tests for Colony directed mode (--mission flag)."""

from unittest.mock import patch

import pytest
import yaml

from fliiq.runtime.colony.agents import _build_system_prompt
from fliiq.runtime.colony.models import (
    AgentRole,
    ColonyConfig,
    ColonyState,
    GovernanceDecision,
    IntelBrief,
    Proposal,
    ProposalStatus,
    ProposalType,
    RiskLevel,
)
from fliiq.runtime.colony.prompts import GOVERNANCE_MISSION_COMPLETION, mission_preamble
from fliiq.runtime.colony.reporting import ReportGenerator
from fliiq.runtime.colony.state import StateManager


@pytest.fixture()
def state(tmp_path):
    sm = StateManager(tmp_path)
    sm.ensure_dirs()
    return sm


# ── Model Tests ──────────────────────────────────────────────────────


class TestMissionModels:
    def test_colony_config_mission_default_empty(self):
        c = ColonyConfig()
        assert c.mission == ""

    def test_colony_config_with_mission(self):
        c = ColonyConfig(mission="Build 100 skills")
        assert c.mission == "Build 100 skills"

    def test_colony_config_directed_cooldowns_default(self):
        c = ColonyConfig()
        assert c.directed_cooldowns["intelligence"] == 600
        assert c.directed_cooldowns["research"] == 300
        assert c.directed_cooldowns["scout"] == 300
        assert c.directed_cooldowns["qa"] == 300

    def test_colony_config_directed_max_iterations(self):
        c = ColonyConfig()
        assert c.directed_max_iterations == 75

    def test_colony_state_mission_default_empty(self):
        s = ColonyState(experiment_id=1, branch_name="test")
        assert s.mission == ""

    def test_colony_state_with_mission(self):
        s = ColonyState(experiment_id=1, branch_name="test", mission="Research benchmarks")
        assert s.mission == "Research benchmarks"


# ── Prompt Tests ──────────────────────────────────────────────────────


class TestMissionPreamble:
    def test_contains_mission_text(self):
        result = mission_preamble("Build 50 playbooks")
        assert "Build 50 playbooks" in result
        assert "MISSION DIRECTIVE" in result

    def test_contains_role_instructions(self):
        result = mission_preamble("Test mission")
        assert "Intelligence" in result
        assert "Scout" in result
        assert "Research" in result
        assert "QA" in result
        assert "Governance" in result

    def test_governance_completion_text(self):
        assert "mission_complete" in GOVERNANCE_MISSION_COMPLETION
        assert "mission_status.yaml" in GOVERNANCE_MISSION_COMPLETION


# ── Agent Prompt Injection Tests ──────────────────────────────────────


class TestMissionPromptInjection:
    def test_no_mission_no_preamble(self, state, tmp_path):
        (tmp_path.parent / "README.md").write_text("# Test")
        prompt = _build_system_prompt(AgentRole.INTELLIGENCE, state, tmp_path.parent)
        assert "MISSION DIRECTIVE" not in prompt

    def test_mission_prepended_intelligence(self, state, tmp_path):
        (tmp_path.parent / "README.md").write_text("# Test")
        prompt = _build_system_prompt(
            AgentRole.INTELLIGENCE, state, tmp_path.parent,
            mission="Build 100 skills",
        )
        assert "MISSION DIRECTIVE" in prompt
        assert "Build 100 skills" in prompt
        assert "Intelligence Agent" in prompt  # original prompt still there

    def test_mission_prepended_scout(self, state, tmp_path):
        (tmp_path.parent / "README.md").write_text("# Test")
        prompt = _build_system_prompt(
            AgentRole.SCOUT, state, tmp_path.parent,
            mission="Research AI benchmarks",
        )
        assert "MISSION DIRECTIVE" in prompt
        assert "Research AI benchmarks" in prompt
        assert "Scout Agent" in prompt

    def test_mission_prepended_research(self, state, tmp_path):
        (tmp_path.parent / "README.md").write_text("# Test")
        prompt = _build_system_prompt(
            AgentRole.RESEARCH, state, tmp_path.parent,
            mission="Test",
        )
        assert "MISSION DIRECTIVE" in prompt
        assert "Research Agent" in prompt

    def test_mission_prepended_qa(self, state, tmp_path):
        (tmp_path.parent / "README.md").write_text("# Test")
        with patch("fliiq.runtime.colony.git_ops.get_diff_summary", return_value=""):
            prompt = _build_system_prompt(
                AgentRole.QA, state, tmp_path.parent,
                mission="Test",
            )
        assert "MISSION DIRECTIVE" in prompt
        assert "QA Agent" in prompt

    def test_governance_gets_completion_instructions(self, state, tmp_path):
        (tmp_path.parent / "README.md").write_text("# Test")
        prompt = _build_system_prompt(
            AgentRole.GOVERNANCE, state, tmp_path.parent,
            mission="Build 100 skills",
        )
        assert "MISSION DIRECTIVE" in prompt
        assert "mission_complete" in prompt  # from GOVERNANCE_MISSION_COMPLETION
        assert "Governance Agent" in prompt

    def test_non_governance_no_completion_instructions(self, state, tmp_path):
        (tmp_path.parent / "README.md").write_text("# Test")
        prompt = _build_system_prompt(
            AgentRole.SCOUT, state, tmp_path.parent,
            mission="Build skills",
        )
        assert "mission_complete" not in prompt

    def test_all_roles_get_mission(self, state, tmp_path):
        (tmp_path.parent / "README.md").write_text("# Test")
        for role in AgentRole:
            with patch("fliiq.runtime.colony.git_ops.get_diff_summary", return_value=""):
                prompt = _build_system_prompt(
                    role, state, tmp_path.parent,
                    mission="Universal mission",
                )
            assert "MISSION DIRECTIVE" in prompt, f"Missing for {role}"
            assert "Universal mission" in prompt, f"Mission text missing for {role}"


# ── Orchestrator Completion Detection Tests ───────────────────────────


class TestStallDetection:
    def test_stall_after_three_empty_rotations(self):
        from fliiq.runtime.colony.orchestrator import Orchestrator

        config = ColonyConfig(mission="Test")
        orch = Orchestrator.__new__(Orchestrator)
        orch._config = config
        orch._stall_count = 0
        orch._last_artifact_snapshot = None
        orch._state = StateManager.__new__(StateManager)

        # Mock _count_artifacts to return same value each time
        counts = {"proposals": 5, "briefs": 2, "decisions": 3, "votes": 4}
        with patch.object(orch, "_count_artifacts", return_value=counts):
            # First call: sets baseline
            assert orch._check_stall() is False
            # Second call: same counts, stall_count = 1
            assert orch._check_stall() is False
            assert orch._stall_count == 1
            # Third call: stall_count = 2
            assert orch._check_stall() is False
            assert orch._stall_count == 2
            # Fourth call: stall_count = 3 -> stall detected
            assert orch._check_stall() is True

    def test_stall_resets_on_new_artifact(self):
        from fliiq.runtime.colony.orchestrator import Orchestrator

        config = ColonyConfig(mission="Test")
        orch = Orchestrator.__new__(Orchestrator)
        orch._config = config
        orch._stall_count = 0
        orch._last_artifact_snapshot = None

        counts_v1 = {"proposals": 5, "briefs": 2, "decisions": 3, "votes": 4}
        counts_v2 = {"proposals": 6, "briefs": 2, "decisions": 3, "votes": 4}

        with patch.object(orch, "_count_artifacts", return_value=counts_v1):
            orch._check_stall()  # baseline
            orch._check_stall()  # stall 1
            assert orch._stall_count == 1

        with patch.object(orch, "_count_artifacts", return_value=counts_v2):
            orch._check_stall()  # new proposal -> reset
            assert orch._stall_count == 0


class TestMissionCompleteCheck:
    def test_no_file_returns_false(self, tmp_path):
        from fliiq.runtime.colony.orchestrator import Orchestrator

        orch = Orchestrator.__new__(Orchestrator)
        orch._colony_dir = tmp_path
        assert orch._check_mission_complete() is False

    def test_complete_file_returns_true(self, tmp_path):
        from fliiq.runtime.colony.orchestrator import Orchestrator

        orch = Orchestrator.__new__(Orchestrator)
        orch._colony_dir = tmp_path
        state_dir = tmp_path / "state"
        state_dir.mkdir(parents=True, exist_ok=True)
        (state_dir / "mission_status.yaml").write_text(yaml.dump({
            "mission_complete": True,
            "rationale": "All skills built",
            "summary": "Built 20 skills",
        }))
        assert orch._check_mission_complete() is True

    def test_incomplete_file_returns_false(self, tmp_path):
        from fliiq.runtime.colony.orchestrator import Orchestrator

        orch = Orchestrator.__new__(Orchestrator)
        orch._colony_dir = tmp_path
        state_dir = tmp_path / "state"
        state_dir.mkdir(parents=True, exist_ok=True)
        (state_dir / "mission_status.yaml").write_text(yaml.dump({
            "mission_complete": False,
        }))
        assert orch._check_mission_complete() is False


# ── Orchestrator Trigger Lists Tests ──────────────────────────────────


class TestOrchestratorTriggers:
    def _make_orchestrator(self, mission=""):
        from fliiq.runtime.colony.orchestrator import Orchestrator

        config = ColonyConfig(mission=mission)
        orch = Orchestrator.__new__(Orchestrator)
        orch._config = config
        return orch

    def test_auto_bootstrap_order(self):
        orch = self._make_orchestrator()
        order = orch._auto_bootstrap_order()
        assert len(order) == 5
        roles = [r for r, _ in order]
        assert roles[0] == AgentRole.INTELLIGENCE
        assert roles[-1] == AgentRole.GOVERNANCE

    def test_directed_bootstrap_order_includes_mission(self):
        orch = self._make_orchestrator("Build 100 skills")
        order = orch._directed_bootstrap_order()
        assert len(order) == 5
        for _role, trigger in order:
            if _role != AgentRole.QA:  # QA trigger doesn't include mission
                assert "Build 100 skills" in trigger

    def test_auto_scanning_agents(self):
        orch = self._make_orchestrator()
        agents = orch._auto_scanning_agents()
        assert len(agents) == 4
        roles = [r for r, _ in agents]
        assert AgentRole.GOVERNANCE not in roles

    def test_directed_scanning_agents_includes_mission(self):
        orch = self._make_orchestrator("Research benchmarks")
        agents = orch._directed_scanning_agents()
        assert len(agents) == 4
        for _role, trigger in agents:
            if _role != AgentRole.QA:
                assert "Research benchmarks" in trigger


# ── CLI Duration Tests ────────────────────────────────────────────────


class TestCLIDurationLogic:
    """Test the duration capping logic from colony.py start command."""

    def test_directed_default_duration(self):
        # When mission set and duration is default (24.0), should become 1.0
        duration = 24.0
        effective = min(duration, 1.0) if duration != 24.0 else 1.0
        assert effective == 1.0

    def test_directed_custom_short_duration(self):
        # When user specifies shorter, respect it
        duration = 0.5
        effective = min(duration, 1.0) if duration != 24.0 else 1.0
        assert effective == 0.5

    def test_directed_capped_at_1h(self):
        # When user specifies longer, cap at 1h
        duration = 5.0
        effective = min(duration, 1.0) if duration != 24.0 else 1.0
        assert effective == 1.0


# ── Reporting: Mission Deliverable Tests ──────────────────────────────


class TestMissionDeliverable:
    def _populate_state(self, state):
        state.save_colony_state(ColonyState(
            experiment_id=1,
            branch_name="colony/experiment-001",
            status="stopped",
            mission="Build 5 test skills",
            cycle_count=8,
        ))
        state.save_proposal(Proposal(
            id="PROP-DIR001",
            title="fetch_weather skill",
            type=ProposalType.NEW_SKILL,
            risk=RiskLevel.LOW,
            proposer=AgentRole.SCOUT,
            status=ProposalStatus.IMPLEMENTED,
            description="Weather fetching skill using OpenWeatherMap API.",
        ))
        state.save_proposal(Proposal(
            id="PROP-DIR002",
            title="stock_price skill",
            type=ProposalType.NEW_SKILL,
            risk=RiskLevel.LOW,
            proposer=AgentRole.SCOUT,
            status=ProposalStatus.REJECTED,
            description="Stock price skill — rejected due to API cost.",
        ))
        state.append_decision(GovernanceDecision(
            proposal_id="PROP-DIR001",
            outcome=ProposalStatus.APPROVED,
            reasoning="Low risk, useful skill, API is free tier.",
            commit_sha="def456",
        ))
        state.append_decision(GovernanceDecision(
            proposal_id="PROP-DIR002",
            outcome=ProposalStatus.REJECTED,
            reasoning="API requires paid subscription, not worth it.",
        ))
        state.save_intel_brief(IntelBrief(
            id="INTEL-DIR001",
            title="OpenWeatherMap free tier available",
            source_type="blog",
            relevance="direct",
            summary="Free tier allows 1000 calls/day.",
        ))

    def test_generates_deliverable_file(self, state, tmp_path):
        self._populate_state(state)
        gen = ReportGenerator(state, tmp_path)
        path = gen.generate_mission_deliverable(1, "Build 5 test skills")
        assert path.exists()
        assert path.name == "mission-deliverable-001.md"

    def test_deliverable_contains_mission(self, state, tmp_path):
        self._populate_state(state)
        gen = ReportGenerator(state, tmp_path)
        path = gen.generate_mission_deliverable(1, "Build 5 test skills")
        content = path.read_text()
        assert "Build 5 test skills" in content

    def test_deliverable_contains_approved(self, state, tmp_path):
        self._populate_state(state)
        gen = ReportGenerator(state, tmp_path)
        path = gen.generate_mission_deliverable(1, "Build 5 test skills")
        content = path.read_text()
        assert "fetch_weather" in content
        assert "Approved" in content

    def test_deliverable_contains_rejected(self, state, tmp_path):
        self._populate_state(state)
        gen = ReportGenerator(state, tmp_path)
        path = gen.generate_mission_deliverable(1, "Build 5 test skills")
        content = path.read_text()
        assert "stock_price" in content
        assert "Rejected" in content

    def test_deliverable_contains_intelligence(self, state, tmp_path):
        self._populate_state(state)
        gen = ReportGenerator(state, tmp_path)
        path = gen.generate_mission_deliverable(1, "Build 5 test skills")
        content = path.read_text()
        assert "OpenWeatherMap" in content

    def test_deliverable_contains_stats(self, state, tmp_path):
        self._populate_state(state)
        gen = ReportGenerator(state, tmp_path)
        path = gen.generate_mission_deliverable(1, "Build 5 test skills")
        content = path.read_text()
        assert "Proposals total: 2" in content

    def test_deliverable_with_governance_rationale(self, state, tmp_path):
        self._populate_state(state)
        # Add mission_status.yaml
        status_dir = state._dir / "state"
        status_dir.mkdir(parents=True, exist_ok=True)
        (status_dir / "mission_status.yaml").write_text(yaml.dump({
            "mission_complete": True,
            "rationale": "All 5 skills have been proposed and reviewed.",
            "summary": "Built 1 skill, rejected 1, 3 more needed in next session.",
        }))
        gen = ReportGenerator(state, tmp_path)
        path = gen.generate_mission_deliverable(1, "Build 5 test skills")
        content = path.read_text()
        assert "All 5 skills have been proposed" in content
        assert "Built 1 skill" in content


# ── Session Report Mission Header Tests ───────────────────────────────


class TestSessionReportMissionHeader:
    def test_session_report_includes_mission(self, state, tmp_path):
        state.save_colony_state(ColonyState(
            experiment_id=1,
            branch_name="colony/experiment-001",
            status="stopped",
            mission="Build 10 skills",
            cycle_count=5,
        ))
        gen = ReportGenerator(state, tmp_path)
        path = gen.generate_session_report(1)
        content = path.read_text()
        assert "Build 10 skills" in content

    def test_session_report_no_mission_line_when_empty(self, state, tmp_path):
        state.save_colony_state(ColonyState(
            experiment_id=1,
            branch_name="colony/experiment-001",
            status="stopped",
            cycle_count=5,
        ))
        gen = ReportGenerator(state, tmp_path)
        path = gen.generate_session_report(1)
        content = path.read_text()
        assert "**Mission:**" not in content


# ── Escalation Mission Tests ─────────────────────────────────────────


class TestEscalationMission:
    @pytest.mark.asyncio()
    async def test_notify_started_with_mission(self):
        from fliiq.runtime.colony.escalation import notify_started

        with patch("fliiq.runtime.colony.escalation.send_escalation") as mock:
            mock.return_value = True
            await notify_started("123", "colony/exp-001", 1.0, mission="Build skills")
            mock.assert_called_once()
            msg = mock.call_args[0][1]
            assert "Build skills" in msg

    @pytest.mark.asyncio()
    async def test_notify_started_no_mission(self):
        from fliiq.runtime.colony.escalation import notify_started

        with patch("fliiq.runtime.colony.escalation.send_escalation") as mock:
            mock.return_value = True
            await notify_started("123", "colony/exp-001", 24.0)
            msg = mock.call_args[0][1]
            assert "Mission" not in msg


# ── Session Continuation (Previous Deliverable) Tests ────────────────


class TestPreviousDeliverableInjection:
    def test_preamble_no_previous_context(self):
        result = mission_preamble("Build 100 skills")
        assert "Previous Session Results" not in result
        assert "Build 100 skills" in result

    def test_preamble_with_previous_context(self):
        prev = "# Deliverable\n\nBuilt 20 skills so far."
        result = mission_preamble("Build 100 skills", previous_context=prev)
        assert "Previous Session Results" in result
        assert "Built 20 skills so far" in result
        assert "Do NOT re-propose" in result

    def test_previous_context_in_agent_prompt(self, state, tmp_path):
        (tmp_path.parent / "README.md").write_text("# Test")
        prompt = _build_system_prompt(
            AgentRole.SCOUT, state, tmp_path.parent,
            mission="Build 100 skills",
            previous_context="Previously built 20 skills.",
        )
        assert "MISSION DIRECTIVE" in prompt
        assert "Previous Session Results" in prompt
        assert "Previously built 20 skills." in prompt
        assert "Scout Agent" in prompt

    def test_no_previous_context_no_section_in_agent_prompt(self, state, tmp_path):
        (tmp_path.parent / "README.md").write_text("# Test")
        prompt = _build_system_prompt(
            AgentRole.INTELLIGENCE, state, tmp_path.parent,
            mission="Research benchmarks",
        )
        assert "MISSION DIRECTIVE" in prompt
        assert "Previous Session Results" not in prompt

    def test_previous_context_empty_string_no_section(self):
        result = mission_preamble("Test", previous_context="")
        assert "Previous Session Results" not in result

    def test_orchestrator_loads_previous_deliverable(self, tmp_path):
        from fliiq.runtime.colony.orchestrator import Orchestrator

        colony_dir = tmp_path / ".colony"
        reports_dir = colony_dir / "reports"
        reports_dir.mkdir(parents=True)

        # Write two deliverables — orchestrator should pick the latest
        (reports_dir / "mission-deliverable-001.md").write_text("First session results.")
        (reports_dir / "mission-deliverable-002.md").write_text("Second session results.")

        orch = Orchestrator.__new__(Orchestrator)
        orch._colony_dir = colony_dir
        orch._previous_context = ""

        # Simulate the deliverable loading logic from _bootstrap
        deliverables = sorted(reports_dir.glob("mission-deliverable-*.md"))
        if deliverables:
            orch._previous_context = deliverables[-1].read_text()

        assert orch._previous_context == "Second session results."

    def test_orchestrator_no_deliverable_empty_context(self, tmp_path):
        from fliiq.runtime.colony.orchestrator import Orchestrator

        colony_dir = tmp_path / ".colony"
        reports_dir = colony_dir / "reports"
        reports_dir.mkdir(parents=True)

        orch = Orchestrator.__new__(Orchestrator)
        orch._colony_dir = colony_dir
        orch._previous_context = ""

        deliverables = sorted(reports_dir.glob("mission-deliverable-*.md"))
        if deliverables:
            orch._previous_context = deliverables[-1].read_text()

        assert orch._previous_context == ""


# ── State Isolation Tests ────────────────────────────────────────────


class TestStateIsolation:
    def test_auto_mode_uses_state_subdir(self, tmp_path):
        sm = StateManager(tmp_path)
        assert sm._state_dir == tmp_path / "state"

    def test_directed_mode_uses_missions_subdir(self, tmp_path):
        sm = StateManager(tmp_path, state_subdir="missions")
        assert sm._state_dir == tmp_path / "missions"

    def test_reflections_shared_location(self, tmp_path):
        """Both modes write reflections to colony_dir/reflections/, not state_dir."""
        from fliiq.runtime.colony.models import AgentReflection, AgentRole

        auto_sm = StateManager(tmp_path)
        auto_sm.ensure_dirs()
        directed_sm = StateManager(tmp_path, state_subdir="missions")
        directed_sm.ensure_dirs()

        # Auto mode writes a reflection
        auto_sm.append_reflection(AgentReflection(
            agent=AgentRole.INTELLIGENCE,
            cycle_number=1,
            observation="Found new tool",
            action_taken="Wrote intel brief",
            outcome="Wrote brief",
        ))

        # Directed mode can read it
        reflections = directed_sm.get_reflections("intelligence", limit=5)
        assert len(reflections) == 1
        assert reflections[0].observation == "Found new tool"

    def test_reset_working_state_only_clears_own_dir(self, tmp_path):
        """Directed mode reset doesn't touch auto mode state."""
        auto_sm = StateManager(tmp_path)
        auto_sm.ensure_dirs()

        # Create auto mode proposal
        auto_sm.save_proposal(Proposal(
            id="PROP-AUTO01",
            title="Auto proposal",
            type=ProposalType.NEW_SKILL,
            risk=RiskLevel.LOW,
            proposer=AgentRole.SCOUT,
            status=ProposalStatus.PROPOSED,
            description="From auto mode.",
        ))

        # Directed mode resets its own state
        directed_sm = StateManager(tmp_path, state_subdir="missions")
        directed_sm.ensure_dirs()
        directed_sm.reset_working_state()

        # Auto mode proposal still exists
        assert len(auto_sm.list_proposals()) == 1
        assert auto_sm.list_proposals()[0].id == "PROP-AUTO01"

    def test_modes_have_independent_proposals(self, tmp_path):
        auto_sm = StateManager(tmp_path)
        auto_sm.ensure_dirs()
        directed_sm = StateManager(tmp_path, state_subdir="missions")
        directed_sm.ensure_dirs()

        auto_sm.save_proposal(Proposal(
            id="PROP-AUTO01",
            title="Auto proposal",
            type=ProposalType.NEW_SKILL,
            risk=RiskLevel.LOW,
            proposer=AgentRole.SCOUT,
            status=ProposalStatus.PROPOSED,
            description="Auto.",
        ))

        directed_sm.save_proposal(Proposal(
            id="PROP-DIR01",
            title="Directed proposal",
            type=ProposalType.NEW_SKILL,
            risk=RiskLevel.LOW,
            proposer=AgentRole.SCOUT,
            status=ProposalStatus.PROPOSED,
            description="Directed.",
        ))

        assert len(auto_sm.list_proposals()) == 1
        assert auto_sm.list_proposals()[0].id == "PROP-AUTO01"
        assert len(directed_sm.list_proposals()) == 1
        assert directed_sm.list_proposals()[0].id == "PROP-DIR01"

    def test_reports_shared_between_modes(self, tmp_path):
        """Both modes write to colony_dir/reports/."""
        auto_sm = StateManager(tmp_path)
        auto_sm.ensure_dirs()
        directed_sm = StateManager(tmp_path, state_subdir="missions")
        directed_sm.ensure_dirs()

        # Both should reference the same reports directory
        assert (tmp_path / "reports").is_dir()
        # Reports dir is _dir / "reports", same for both
        assert auto_sm._dir == directed_sm._dir
