"""Tests for agent assignment algorithm.

Test scenarios:
1. Assignment for low/medium/high difficulty issues
2. Context capacity filtering (50% rule enforcement)
3. Cost optimization logic
4. Error handling for impossible assignments
"""

import pytest

from src.agent_assignment import NoCapableAgentError, assign_agent
from src.models import AGENT_PROFILES, AgentName, Capability


class TestAgentAssignment:
    """Test the intelligent agent assignment algorithm."""

    def test_assign_low_difficulty_prefers_cheapest(self) -> None:
        """Test that low difficulty issues get assigned to cheapest capable agent."""
        # For low difficulty with small context (25K tokens), expect cheapest self-hosted
        # Both GLM and minimax are cost=0, GLM comes first alphabetically
        assigned = assign_agent(
            estimated_context=25000,
            difficulty="easy"
        )
        assert assigned == AgentName.GLM

    def test_assign_low_difficulty_large_context_uses_haiku(self) -> None:
        """Test that low difficulty with larger context uses Haiku."""
        # minimax and GLM have 128K limit (can handle up to 64K)
        # 100K * 2 (50% rule) = needs 200K capacity
        # Should use Haiku (200K context, cheapest commercial for low)
        assigned = assign_agent(
            estimated_context=100000,
            difficulty="easy"
        )
        assert assigned == AgentName.HAIKU

    def test_assign_low_difficulty_within_self_hosted_uses_glm(self) -> None:
        """Test that low difficulty within self-hosted capacity uses GLM."""
        # 60K tokens needs 120K capacity (50% rule)
        # GLM has 128K limit (can handle up to 64K)
        # Should use GLM (self-hosted, cost=0)
        assigned = assign_agent(
            estimated_context=60000,
            difficulty="easy"
        )
        assert assigned == AgentName.GLM

    def test_assign_medium_difficulty_prefers_glm(self) -> None:
        """Test that medium difficulty prefers self-hosted GLM when possible."""
        # GLM is self-hosted (cost=0) and can handle medium difficulty
        assigned = assign_agent(
            estimated_context=30000,
            difficulty="medium"
        )
        assert assigned == AgentName.GLM

    def test_assign_medium_difficulty_large_context_uses_sonnet(self) -> None:
        """Test that medium difficulty with large context uses Sonnet."""
        # 80K tokens needs 160K capacity (50% rule)
        # GLM has 128K limit (can handle up to 64K)
        # Should use Sonnet (200K context, cheapest commercial for medium)
        assigned = assign_agent(
            estimated_context=80000,
            difficulty="medium"
        )
        assert assigned == AgentName.SONNET

    def test_assign_high_difficulty_uses_opus(self) -> None:
        """Test that high difficulty always uses Opus."""
        # Only Opus can handle high difficulty
        assigned = assign_agent(
            estimated_context=50000,
            difficulty="hard"
        )
        assert assigned == AgentName.OPUS

    def test_assign_high_difficulty_large_context_uses_opus(self) -> None:
        """Test that high difficulty with large context still uses Opus."""
        # Even with large context, Opus is the only option for high difficulty
        assigned = assign_agent(
            estimated_context=90000,
            difficulty="hard"
        )
        assert assigned == AgentName.OPUS

    def test_fifty_percent_rule_enforced(self) -> None:
        """Test that 50% context capacity rule is strictly enforced."""
        # 65K tokens needs 130K capacity (50% rule)
        # GLM has 128K limit, so can't handle this
        # Should use Sonnet (200K limit, can handle up to 100K)
        assigned = assign_agent(
            estimated_context=65000,
            difficulty="medium"
        )
        assert assigned == AgentName.SONNET

    def test_self_hosted_preferred_when_capable(self) -> None:
        """Test that self-hosted agents are preferred over commercial when capable."""
        # For medium difficulty with 30K context:
        # GLM (self-hosted, cost=0) can handle it
        # Sonnet (commercial, cost=3.0) can also handle it
        # Should prefer GLM
        assigned = assign_agent(
            estimated_context=30000,
            difficulty="medium"
        )
        assert assigned == AgentName.GLM

    def test_impossible_assignment_raises_error(self) -> None:
        """Test that impossible assignments raise NoCapableAgentError."""
        # No agent can handle 150K tokens (needs 300K capacity with 50% rule)
        # Max capacity is 200K (Opus, Sonnet, Haiku)
        with pytest.raises(NoCapableAgentError) as exc_info:
            assign_agent(
                estimated_context=150000,
                difficulty="medium"
            )
        assert "No capable agent found" in str(exc_info.value)
        assert "150000" in str(exc_info.value)

    def test_impossible_assignment_high_difficulty_massive_context(self) -> None:
        """Test error when even Opus cannot handle the context."""
        # Opus has 200K limit, so can handle up to 100K with 50% rule
        # This should fail
        with pytest.raises(NoCapableAgentError) as exc_info:
            assign_agent(
                estimated_context=120000,
                difficulty="hard"
            )
        assert "No capable agent found" in str(exc_info.value)

    def test_edge_case_exact_fifty_percent(self) -> None:
        """Test edge case where context exactly meets 50% threshold."""
        # 100K tokens needs exactly 200K capacity
        # Haiku, Sonnet, Opus all have 200K
        # For low difficulty, should use Haiku (cheapest)
        assigned = assign_agent(
            estimated_context=100000,
            difficulty="easy"
        )
        # GLM can only handle 64K (128K / 2), so needs commercial
        assert assigned == AgentName.HAIKU

    def test_agent_selection_by_cost_ordering(self) -> None:
        """Test that agents are selected by cost when multiple are capable."""
        # For low difficulty with 20K context, multiple agents qualify:
        # - GLM (cost=0, 128K limit) - comes first alphabetically
        # - minimax (cost=0, 128K limit)
        # - Haiku (cost=0.8, 200K limit)
        # - Sonnet (cost=3.0, 200K limit)
        # Should pick cheapest: GLM (cost=0, alphabetically first)
        assigned = assign_agent(
            estimated_context=20000,
            difficulty="easy"
        )
        # GLM selected due to alphabetical ordering when costs are equal
        assert assigned == AgentName.GLM

    def test_capability_filtering_excludes_incapable_agents(self) -> None:
        """Test that agents without required capability are excluded."""
        # For medium difficulty:
        # - minimax cannot handle medium (only LOW)
        # - Haiku cannot handle medium (only LOW)
        # Valid options: GLM, Sonnet, Opus
        # Should prefer GLM (self-hosted, cost=0)
        assigned = assign_agent(
            estimated_context=30000,
            difficulty="medium"
        )
        assert assigned == AgentName.GLM
        assert assigned not in [AgentName.MINIMAX, AgentName.HAIKU]

    def test_zero_context_estimate(self) -> None:
        """Test assignment with zero context estimate."""
        # Zero context should work with any agent
        # For low difficulty, should get cheapest (GLM comes first alphabetically)
        assigned = assign_agent(
            estimated_context=0,
            difficulty="easy"
        )
        assert assigned == AgentName.GLM

    def test_small_context_estimate(self) -> None:
        """Test assignment with very small context estimate."""
        # 1K tokens should work with any agent (GLM comes first alphabetically)
        assigned = assign_agent(
            estimated_context=1000,
            difficulty="easy"
        )
        assert assigned == AgentName.GLM


class TestAgentAssignmentEdgeCases:
    """Test edge cases and boundary conditions."""

    def test_difficulty_case_insensitive(self) -> None:
        """Test that difficulty matching is case-insensitive."""
        # Should handle different casings of difficulty
        assigned_lower = assign_agent(estimated_context=30000, difficulty="easy")
        assigned_title = assign_agent(estimated_context=30000, difficulty="easy")
        assert assigned_lower == assigned_title

    def test_max_capacity_for_each_agent(self) -> None:
        """Test maximum handleable context for each agent type."""
        # minimax: 128K / 2 = 64K max
        assigned = assign_agent(estimated_context=64000, difficulty="easy")
        assert assigned in [AgentName.MINIMAX, AgentName.GLM]

        # GLM: 128K / 2 = 64K max
        assigned = assign_agent(estimated_context=64000, difficulty="medium")
        assert assigned == AgentName.GLM

        # Opus: 200K / 2 = 100K max
        assigned = assign_agent(estimated_context=100000, difficulty="hard")
        assert assigned == AgentName.OPUS

    def test_negative_context_raises_error(self) -> None:
        """Test that negative context raises appropriate error."""
        with pytest.raises(ValueError) as exc_info:
            assign_agent(estimated_context=-1000, difficulty="easy")
        assert "negative" in str(exc_info.value).lower()

    def test_invalid_difficulty_raises_error(self) -> None:
        """Test that invalid difficulty raises appropriate error."""
        with pytest.raises(ValueError) as exc_info:
            assign_agent(estimated_context=30000, difficulty="invalid")  # type: ignore
        assert "difficulty" in str(exc_info.value).lower()


class TestAgentAssignmentIntegration:
    """Integration tests with actual agent profiles."""

    def test_uses_actual_agent_profiles(self) -> None:
        """Test that assignment uses actual AGENT_PROFILES data."""
        assigned = assign_agent(estimated_context=30000, difficulty="medium")
        assert assigned in AGENT_PROFILES
        profile = AGENT_PROFILES[assigned]
        assert profile.context_limit >= 60000  # 30K * 2 for 50% rule

    def test_all_difficulty_levels_have_assignments(self) -> None:
        """Test that all difficulty levels can be assigned for reasonable contexts."""
        # Test each difficulty level
        easy_agent = assign_agent(estimated_context=30000, difficulty="easy")
        assert easy_agent in AGENT_PROFILES

        medium_agent = assign_agent(estimated_context=30000, difficulty="medium")
        assert medium_agent in AGENT_PROFILES

        hard_agent = assign_agent(estimated_context=30000, difficulty="hard")
        assert hard_agent in AGENT_PROFILES

    def test_cost_optimization_verified_with_profiles(self) -> None:
        """Test that cost optimization actually selects cheaper agents."""
        # For medium difficulty with 30K context:
        # GLM (cost=0) should be selected over Sonnet (cost=3.0)
        assigned = assign_agent(estimated_context=30000, difficulty="medium")
        assigned_cost = AGENT_PROFILES[assigned].cost_per_mtok
        assert assigned_cost == 0.0  # Self-hosted


class TestCostOptimizationScenarios:
    """Test scenarios from COORD-006 validating cost optimization.

    These tests validate that the assignment algorithm optimizes costs
    by selecting the cheapest capable agent for each scenario.
    """

    def test_low_difficulty_assigns_minimax_or_glm(self) -> None:
        """Test: Low difficulty issue assigns to MiniMax or GLM (free/self-hosted).

        Scenario: Small, simple task that can be handled by lightweight agents.
        Expected: Assigns to cost=0 agent (GLM or MiniMax).
        Cost savings: Avoids Haiku ($0.8/Mtok), Sonnet ($3/Mtok), Opus ($15/Mtok).
        """
        # Low difficulty with 10K tokens (needs 20K capacity)
        assigned = assign_agent(estimated_context=10000, difficulty="low")

        # Should assign to self-hosted (cost=0)
        assert assigned in [AgentName.GLM, AgentName.MINIMAX]
        assert AGENT_PROFILES[assigned].cost_per_mtok == 0.0

    def test_low_difficulty_small_context_cost_savings(self) -> None:
        """Test: Low difficulty with small context demonstrates cost savings.

        Validates that for simple tasks, we use free agents instead of commercial.
        Cost analysis: $0 vs $0.8/Mtok (Haiku) = 100% savings.
        """
        assigned = assign_agent(estimated_context=5000, difficulty="easy")
        profile = AGENT_PROFILES[assigned]

        # Verify cost=0 assignment
        assert profile.cost_per_mtok == 0.0

        # Calculate savings vs cheapest commercial option (Haiku)
        haiku_cost = AGENT_PROFILES[AgentName.HAIKU].cost_per_mtok
        savings_percent = 100.0  # Complete savings using self-hosted

        assert savings_percent == 100.0
        assert profile.cost_per_mtok < haiku_cost

    def test_medium_difficulty_assigns_glm_when_capable(self) -> None:
        """Test: Medium difficulty assigns to GLM (self-hosted, free).

        Scenario: Medium complexity task within GLM's capacity.
        Expected: GLM (cost=0) over Sonnet ($3/Mtok).
        Cost savings: 100% vs commercial alternatives.
        """
        # Medium difficulty with 40K tokens (needs 80K capacity)
        # GLM has 128K limit, can handle this
        assigned = assign_agent(estimated_context=40000, difficulty="medium")

        assert assigned == AgentName.GLM
        assert AGENT_PROFILES[assigned].cost_per_mtok == 0.0

    def test_medium_difficulty_glm_cost_optimization(self) -> None:
        """Test: Medium difficulty demonstrates GLM cost optimization.

        Validates cost savings when using self-hosted GLM vs commercial Sonnet.
        Cost analysis: $0 vs $3/Mtok (Sonnet) = 100% savings.
        """
        assigned = assign_agent(estimated_context=50000, difficulty="medium")
        profile = AGENT_PROFILES[assigned]

        # Should use GLM (self-hosted)
        assert assigned == AgentName.GLM
        assert profile.cost_per_mtok == 0.0

        # Calculate savings vs Sonnet
        sonnet_cost = AGENT_PROFILES[AgentName.SONNET].cost_per_mtok
        cost_per_100k_tokens = (sonnet_cost / 1_000_000) * 100_000

        # Savings: using free agent instead of $0.30 per 100K tokens
        assert cost_per_100k_tokens == 0.3
        assert profile.cost_per_mtok == 0.0

    def test_high_difficulty_assigns_opus_only_capable(self) -> None:
        """Test: High difficulty assigns to Opus (only capable agent).

        Scenario: Complex task requiring advanced reasoning.
        Expected: Opus (only agent with HIGH capability).
        Note: No cost optimization possible - Opus is required.
        """
        # High difficulty with 70K tokens
        assigned = assign_agent(estimated_context=70000, difficulty="high")

        assert assigned == AgentName.OPUS
        assert Capability.HIGH in AGENT_PROFILES[assigned].capabilities

    def test_high_difficulty_opus_required_no_alternative(self) -> None:
        """Test: High difficulty has no cheaper alternative.

        Validates that Opus is the only option for high difficulty tasks.
        This scenario demonstrates when cost optimization doesn't apply.
        """
        assigned = assign_agent(estimated_context=30000, difficulty="hard")

        # Only Opus can handle high difficulty
        assert assigned == AgentName.OPUS

        # Verify no other agent has HIGH capability
        for agent_name, profile in AGENT_PROFILES.items():
            if agent_name != AgentName.OPUS:
                assert Capability.HIGH not in profile.capabilities

    def test_oversized_issue_rejects_no_agent_capacity(self) -> None:
        """Test: Oversized issue is rejected (no agent has capacity).

        Scenario: Task requires more context than any agent can provide.
        Expected: NoCapableAgentError raised.
        Protection: Prevents assigning impossible tasks.
        """
        # 150K tokens needs 300K capacity (50% rule)
        # Max available is 200K (Opus, Sonnet, Haiku)
        with pytest.raises(NoCapableAgentError) as exc_info:
            assign_agent(estimated_context=150000, difficulty="medium")

        error = exc_info.value
        assert error.estimated_context == 150000
        assert "No capable agent found" in str(error)

    def test_oversized_issue_provides_actionable_error(self) -> None:
        """Test: Oversized issue provides clear error message.

        Validates that error message suggests breaking down the issue.
        """
        with pytest.raises(NoCapableAgentError) as exc_info:
            assign_agent(estimated_context=200000, difficulty="low")

        error_message = str(exc_info.value)
        assert "200000" in error_message
        assert "breaking down" in error_message.lower()

    def test_cost_optimization_across_all_scenarios(self) -> None:
        """Test: Validate cost optimization across all common scenarios.

        This comprehensive test validates the entire cost optimization strategy
        by testing multiple representative scenarios and calculating aggregate savings.
        """
        scenarios = [
            # (context, difficulty, expected_agent, scenario_name)
            (10_000, "low", AgentName.GLM, "Simple task"),
            (40_000, "medium", AgentName.GLM, "Medium task (GLM capacity)"),
            (70_000, "medium", AgentName.SONNET, "Medium task (needs commercial)"),
            (50_000, "high", AgentName.OPUS, "Complex task"),
        ]

        total_cost_optimized = 0.0
        total_cost_naive = 0.0

        for context, difficulty, expected, scenario_name in scenarios:
            # Get optimized assignment
            assigned = assign_agent(estimated_context=context, difficulty=difficulty)
            optimized_cost = AGENT_PROFILES[assigned].cost_per_mtok

            # Calculate naive cost (using most expensive capable agent)
            capability = (Capability.HIGH if difficulty == "high"
                         else Capability.MEDIUM if difficulty == "medium"
                         else Capability.LOW)

            # Find most expensive capable agent that can handle context
            capable_agents = [
                p for p in AGENT_PROFILES.values()
                if capability in p.capabilities and p.context_limit >= context * 2
            ]
            naive_cost = max(p.cost_per_mtok for p in capable_agents) if capable_agents else 0.0

            # Accumulate costs per million tokens
            total_cost_optimized += optimized_cost
            total_cost_naive += naive_cost

            # Verify we assigned the expected agent
            assert assigned == expected, f"Failed for scenario: {scenario_name}"

        # Calculate savings
        if total_cost_naive > 0:
            savings_percent = ((total_cost_naive - total_cost_optimized) /
                              total_cost_naive * 100)
        else:
            savings_percent = 0.0

        # Should see significant cost savings
        assert savings_percent >= 50.0, (
            f"Cost optimization should save at least 50%, saved {savings_percent:.1f}%"
        )

    def test_boundary_conditions_for_cost_optimization(self) -> None:
        """Test: Boundary conditions at capacity limits.

        Validates cost optimization behavior at exact capacity boundaries
        where agent selection switches from self-hosted to commercial.
        """
        # At GLM's exact limit: 64K tokens (128K capacity / 2)
        # Should still use GLM
        assigned_at_limit = assign_agent(estimated_context=64000, difficulty="medium")
        assert assigned_at_limit == AgentName.GLM

        # Just over GLM's limit: 65K tokens (needs 130K capacity)
        # Must use Sonnet (200K capacity)
        assigned_over_limit = assign_agent(estimated_context=65000, difficulty="medium")
        assert assigned_over_limit == AgentName.SONNET

        # Verify cost difference
        glm_cost = AGENT_PROFILES[AgentName.GLM].cost_per_mtok
        sonnet_cost = AGENT_PROFILES[AgentName.SONNET].cost_per_mtok
        assert glm_cost < sonnet_cost
