"""Tests for P2 token optimization."""
from __future__ import annotations

from logos_adk.tool_dedup import deduplicate_tool_schemas


class TestToolDedup:
    def test_removes_exact_duplicates(self):
        schema_a = {"name": "search", "description": "Search", "parameters": {"type": "object", "properties": {"q": {"type": "string"}}}}
        schema_b = {"name": "search", "description": "Search", "parameters": {"type": "object", "properties": {"q": {"type": "string"}}}}
        schema_c = {"name": "calc", "description": "Calculate", "parameters": {"type": "object", "properties": {"expr": {"type": "string"}}}}
        result = deduplicate_tool_schemas([schema_a, schema_b, schema_c])
        assert len(result) == 2
        names = [s["name"] for s in result]
        assert "search" in names
        assert "calc" in names

    def test_keeps_different_tools_with_same_name_different_params(self):
        schema_a = {"name": "search", "description": "Search v1", "parameters": {"type": "object", "properties": {"q": {"type": "string"}}}}
        schema_b = {"name": "search", "description": "Search v2", "parameters": {"type": "object", "properties": {"query": {"type": "string"}, "limit": {"type": "integer"}}}}
        result = deduplicate_tool_schemas([schema_a, schema_b])
        assert len(result) == 2

    def test_empty_input(self):
        assert deduplicate_tool_schemas([]) == []

    def test_none_input(self):
        assert deduplicate_tool_schemas(None) is None

    def test_preserves_order_first_seen(self):
        schemas = [
            {"name": "b_tool", "description": "B"},
            {"name": "a_tool", "description": "A"},
            {"name": "b_tool", "description": "B"},
        ]
        result = deduplicate_tool_schemas(schemas)
        assert [s["name"] for s in result] == ["b_tool", "a_tool"]


from logos_adk.smart_tools import select_relevant_tools


class TestSmartToolSelection:
    def test_selects_tools_matching_keywords(self):
        schemas = [
            {"name": "search_web", "description": "Search the web for information"},
            {"name": "calculate", "description": "Perform math calculations"},
            {"name": "send_email", "description": "Send an email message"},
            {"name": "search_docs", "description": "Search internal documents"},
        ]
        result = select_relevant_tools(schemas, task_hint="search for documents")
        names = [s["name"] for s in result]
        assert "search_web" in names
        assert "search_docs" in names

    def test_returns_all_when_no_hint(self):
        schemas = [
            {"name": "search", "description": "Search"},
            {"name": "calc", "description": "Calculate"},
        ]
        result = select_relevant_tools(schemas, task_hint=None)
        assert len(result) == 2

    def test_returns_all_when_hint_empty(self):
        schemas = [{"name": "a", "description": "A tool"}]
        result = select_relevant_tools(schemas, task_hint="")
        assert len(result) == 1

    def test_always_includes_mandatory_tools(self):
        schemas = [
            {"name": "search", "description": "Search"},
            {"name": "respond", "description": "Send response to user"},
        ]
        result = select_relevant_tools(
            schemas,
            task_hint="calculate math",
            mandatory={"respond"},
        )
        names = [s["name"] for s in result]
        assert "respond" in names

    def test_min_tools_floor(self):
        schemas = [
            {"name": "a", "description": "Alpha tool"},
            {"name": "b", "description": "Beta tool"},
            {"name": "c", "description": "Gamma tool"},
        ]
        result = select_relevant_tools(schemas, task_hint="zzzzz", min_tools=2)
        assert len(result) >= 2

    def test_empty_schemas(self):
        assert select_relevant_tools([], task_hint="anything") == []

    def test_none_schemas(self):
        assert select_relevant_tools(None, task_hint="anything") is None


from logos_adk.semantic_cache import SemanticCache


class TestSemanticCache:
    def test_miss_on_empty_cache(self):
        cache = SemanticCache(max_entries=100)
        result = cache.get("Hello, how are you?", model="test-model")
        assert result is None

    def test_hit_on_exact_match(self):
        cache = SemanticCache(max_entries=100)
        cache.put("Hello", model="m", response="Hi there!", input_tokens=10, output_tokens=5)
        result = cache.get("Hello", model="m")
        assert result is not None
        assert result["response"] == "Hi there!"
        assert result["input_tokens"] == 10
        assert result["output_tokens"] == 5

    def test_miss_on_different_model(self):
        cache = SemanticCache(max_entries=100)
        cache.put("Hello", model="model-a", response="Hi!", input_tokens=5, output_tokens=3)
        result = cache.get("Hello", model="model-b")
        assert result is None

    def test_miss_on_different_content(self):
        cache = SemanticCache(max_entries=100)
        cache.put("Hello", model="m", response="Hi!", input_tokens=5, output_tokens=3)
        result = cache.get("Goodbye", model="m")
        assert result is None

    def test_eviction_when_full(self):
        cache = SemanticCache(max_entries=2)
        cache.put("a", model="m", response="A", input_tokens=1, output_tokens=1)
        cache.put("b", model="m", response="B", input_tokens=1, output_tokens=1)
        cache.put("c", model="m", response="C", input_tokens=1, output_tokens=1)
        assert cache.get("a", model="m") is None
        assert cache.get("b", model="m") is not None
        assert cache.get("c", model="m") is not None

    def test_stats_tracking(self):
        cache = SemanticCache(max_entries=100)
        cache.put("q", model="m", response="a", input_tokens=100, output_tokens=50)
        cache.get("q", model="m")  # hit
        cache.get("miss", model="m")  # miss
        stats = cache.stats()
        assert stats["hits"] == 1
        assert stats["misses"] == 1
        assert stats["tokens_saved"] == 100

    def test_clear(self):
        cache = SemanticCache(max_entries=100)
        cache.put("q", model="m", response="a", input_tokens=10, output_tokens=5)
        cache.clear()
        assert cache.get("q", model="m") is None
        stats = cache.stats()
        assert stats["hits"] == 0

    def test_includes_tool_schemas_in_key(self):
        cache = SemanticCache(max_entries=100)
        tools_a = [{"name": "search"}]
        tools_b = [{"name": "calc"}]
        cache.put("Hello", model="m", response="A", input_tokens=5, output_tokens=3, tool_schemas=tools_a)
        result = cache.get("Hello", model="m", tool_schemas=tools_b)
        assert result is None
        result = cache.get("Hello", model="m", tool_schemas=tools_a)
        assert result is not None


from logos_adk.prompt_dedup import extract_common_prefix, deduplicate_system_prompts


class TestPromptDedup:
    def test_extract_common_prefix_identical(self):
        prompts = ["You are a helpful assistant.", "You are a helpful assistant."]
        prefix = extract_common_prefix(prompts)
        assert prefix == "You are a helpful assistant."

    def test_extract_common_prefix_shared(self):
        prompts = [
            "You are a CRM agent. Handle sales.",
            "You are a CRM agent. Handle support.",
        ]
        prefix = extract_common_prefix(prompts)
        assert prefix == "You are a CRM agent."

    def test_extract_common_prefix_none(self):
        prompts = ["Hello world", "Goodbye moon"]
        prefix = extract_common_prefix(prompts)
        assert prefix == ""

    def test_extract_common_prefix_single(self):
        prefix = extract_common_prefix(["Only one"])
        assert prefix == "Only one"

    def test_extract_common_prefix_empty(self):
        assert extract_common_prefix([]) == ""

    def test_deduplicate_removes_prefix(self):
        prompts = {
            "agent_a": "You are a CRM agent. Handle sales queries.",
            "agent_b": "You are a CRM agent. Handle support tickets.",
        }
        result = deduplicate_system_prompts(prompts, min_prefix_len=10)
        assert result["shared_prefix"] == "You are a CRM agent."
        assert "Handle sales queries." in result["deltas"]["agent_a"]
        assert "Handle support tickets." in result["deltas"]["agent_b"]

    def test_deduplicate_no_dedup_when_prefix_too_short(self):
        prompts = {
            "a": "Hi there, do sales",
            "b": "Hi there, do support",
        }
        result = deduplicate_system_prompts(prompts, min_prefix_len=50)
        assert result["shared_prefix"] == ""
        assert result["deltas"]["a"] == "Hi there, do sales"
        assert result["deltas"]["b"] == "Hi there, do support"


from logos_adk.auto_context import get_model_context_limit, auto_configure_context_window
from logos_adk.config import ContextWindowConfig


class TestAutoContext:
    def test_known_model_limits(self):
        assert get_model_context_limit("claude-sonnet-4-20250514") == 200_000
        assert get_model_context_limit("claude-haiku-4-5-20251001") == 200_000
        assert get_model_context_limit("gpt-4o") == 128_000
        assert get_model_context_limit("gpt-4o-mini") == 128_000
        assert get_model_context_limit("gemini-2.0-flash") == 1_000_000

    def test_unknown_model_returns_default(self):
        assert get_model_context_limit("unknown-model-xyz") == 0

    def test_partial_match(self):
        assert get_model_context_limit("claude-sonnet-4") == 200_000

    def test_auto_configure_sets_limit(self):
        cfg = ContextWindowConfig()
        result = auto_configure_context_window(cfg, model="claude-sonnet-4-20250514")
        assert result.max_context_tokens == 200_000

    def test_auto_configure_noop_when_already_set(self):
        cfg = ContextWindowConfig(max_context_tokens=50_000)
        result = auto_configure_context_window(cfg, model="claude-sonnet-4-20250514")
        assert result.max_context_tokens == 50_000

    def test_auto_configure_noop_for_unknown_model(self):
        cfg = ContextWindowConfig()
        result = auto_configure_context_window(cfg, model="unknown-model")
        assert result.max_context_tokens == 0

    def test_auto_configure_returns_new_config(self):
        cfg = ContextWindowConfig()
        result = auto_configure_context_window(cfg, model="gpt-4o")
        assert result is not cfg
        assert cfg.max_context_tokens == 0


class TestAutoContextIntegration:
    def test_agent_auto_configures_on_begin_caches(self):
        from logos_adk.agent import Agent
        agent = Agent("test").context_window(max_tokens=0, auto_detect_model_limit=True)
        agent._config.model = "claude-sonnet-4-20250514"
        agent._begin_run_caches()
        assert agent._config.context_window.max_context_tokens == 200_000
        agent._end_run_caches()

    def test_agent_no_auto_configure_when_disabled(self):
        from logos_adk.agent import Agent
        agent = Agent("test")
        agent._config.model = "claude-sonnet-4-20250514"
        agent._begin_run_caches()
        assert agent._config.context_window.max_context_tokens == 0
        agent._end_run_caches()


class TestSemanticCacheIntegration:
    def test_agent_creates_cache_when_enabled(self):
        from logos_adk.agent import Agent
        agent = Agent("test").context_window(max_tokens=100_000, semantic_cache_max_entries=128)
        cache = agent._get_semantic_cache()
        assert cache is not None

    def test_agent_no_cache_when_disabled(self):
        from logos_adk.agent import Agent
        agent = Agent("test")
        cache = agent._get_semantic_cache()
        assert cache is None

    def test_agent_cache_is_singleton(self):
        from logos_adk.agent import Agent
        agent = Agent("test").context_window(max_tokens=100_000, semantic_cache_max_entries=128)
        c1 = agent._get_semantic_cache()
        c2 = agent._get_semantic_cache()
        assert c1 is c2


class TestP2PublicAPI:
    def test_semantic_cache_importable(self):
        from logos_adk import SemanticCache
        cache = SemanticCache(max_entries=10)
        assert cache.stats()["size"] == 0

    def test_get_model_context_limit_importable(self):
        from logos_adk import get_model_context_limit
        assert get_model_context_limit("claude-sonnet-4") > 0

    def test_tool_dedup_importable(self):
        from logos_adk.tool_dedup import deduplicate_tool_schemas
        assert deduplicate_tool_schemas([]) == []

    def test_smart_tools_importable(self):
        from logos_adk.smart_tools import select_relevant_tools
        assert select_relevant_tools([], task_hint="x") == []

    def test_prompt_dedup_importable(self):
        from logos_adk.prompt_dedup import extract_common_prefix
        assert extract_common_prefix([]) == ""


class TestP2ConfigParser:
    def test_parse_all_p2_fields(self):
        from logos_adk.config_parser import parse_agent_config_data
        data = {
            "name": "test-agent",
            "context_window": {
                "max_context_tokens": 100_000,
                "smart_tool_selection": True,
                "semantic_cache_max_entries": 256,
                "auto_detect_model_limit": True,
            },
        }
        cfg = parse_agent_config_data(data)
        assert cfg.context_window.smart_tool_selection is True
        assert cfg.context_window.semantic_cache_max_entries == 256
        assert cfg.context_window.auto_detect_model_limit is True

    def test_p2_fields_default_to_off(self):
        from logos_adk.config_parser import parse_agent_config_data
        data = {"name": "test-agent"}
        cfg = parse_agent_config_data(data)
        assert cfg.context_window.smart_tool_selection is False
        assert cfg.context_window.semantic_cache_max_entries == 0
        assert cfg.context_window.auto_detect_model_limit is False
