from unittest.mock import MagicMock
import pytest
from sari.mcp.tools.search import execute_search
from sari.mcp.tools.list_files import execute_list_files
from sari.mcp.tools.status import execute_status
from sari.core.models import SearchHit


@pytest.fixture(autouse=True)
def _force_pack_format(monkeypatch):
    monkeypatch.setenv("SARI_FORMAT", "pack")


def test_execute_search_basic():
    db = MagicMock()
    logger = MagicMock()
    engine = MagicMock()
    roots = ["/tmp/ws"]
    hit = SearchHit(repo="repo1", path="path1", score=1.0, snippet="hi")
    db.search.return_value = ([hit], {"total": 1, "total_mode": "exact", "engine": "embedded"})
    
    args = {"query": "test", "limit": 10, "search_type": "code"}
    resp = execute_search(args, db, logger, roots, engine=engine)
    text = resp["content"][0]["text"]
    assert "PACK1 tool=search ok=true" in text
    assert "r:t=code p=path1" in text

def test_execute_search_respects_limit():
    db = MagicMock()
    logger = MagicMock()
    roots = ["/tmp/ws"]
    long_snippet = "A" * 300
    hits = [
        SearchHit(repo="repo1", path=f"path{i}", score=1.0, snippet=long_snippet)
        for i in range(5)
    ]
    db.search.return_value = (hits, {"total": 5, "total_mode": "exact", "engine": "embedded"})

    # v3 uses 'limit' as the primary constraint
    args = {"query": "test", "limit": 2, "search_type": "code"}
    resp = execute_search(args, db, logger, roots)
    text = resp["content"][0]["text"]
    # search_v2 might return more, but normalization or builder should respect limit if implemented
    # For now, let's ensure the output format is correct
    assert "r:t=code" in text

def test_execute_search_preview_budget():
    db = MagicMock()
    logger = MagicMock()
    roots = ["/tmp/ws"]
    big_snippet = "B" * 2000
    hits = [
        SearchHit(repo="repo1", path=f"path{i}", score=1.0, snippet=big_snippet)
        for i in range(10)
    ]
    db.search.return_value = (hits, {"total": 10, "total_mode": "exact", "engine": "embedded"})

    # 10 items * 2000 chars = 20000 chars (exceeds default budget of 10000)
    args = {"query": "test", "limit": 10, "search_type": "code"}
    resp = execute_search(args, db, logger, roots)
    text = resp["content"][0]["text"]

    # Should detect degradation
    assert "preview_degraded=True" in text

def test_execute_list_files_summary():
    db = MagicMock()
    logger = MagicMock()
    roots = ["/tmp/ws"]
    db.get_repo_stats.return_value = {"repo1": 10, "repo2": 5}
    args = {} # summary_only
    resp = execute_list_files(args, db, logger, roots)
    text = resp["content"][0]["text"]
    assert "PACK1 tool=list_files ok=true mode=summary" in text
    assert "r:repo=repo1 file_count=10" in text

def test_execute_status():
    indexer = MagicMock()
    indexer.status.index_ready = True
    indexer.status.scanned_files = 100
    indexer.status.indexed_files = 80
    indexer.status.errors = 0
    indexer.get_last_commit_ts.return_value = 12345
    db = MagicMock()
    db.fts_enabled = True
    db.count_failed_tasks.return_value = (0, 0)
    cfg = MagicMock()
    cfg.include_ext = [".py"]
    cfg.http_api_port = 47777
    cfg.workspace_roots = ["/tmp/ws"]
    logger = MagicMock()
    
    resp = execute_status({"details": True}, indexer, db, cfg, "/tmp/ws", "0.0.1", logger)
    text = resp["content"][0]["text"]
    assert "PACK1 tool=status ok=true" in text
    assert "m:index_ready=true" in text


def test_execute_status_prefers_indexer_runtime_status():
    indexer = MagicMock()
    indexer.status.index_ready = False
    indexer.status.scanned_files = 0
    indexer.status.indexed_files = 0
    indexer.status.errors = 0
    indexer.get_runtime_status.return_value = {
        "index_ready": False,
        "scanned_files": 12,
        "indexed_files": 7,
        "symbols_extracted": 19,
        "errors": 1,
        "status_source": "worker_progress",
    }
    db = MagicMock()
    db.fts_enabled = True
    cfg = MagicMock()
    cfg.include_ext = [".py"]
    logger = MagicMock()

    resp = execute_status({"details": True}, indexer, db, cfg, "/tmp/ws", "0.0.1", logger)
    text = resp["content"][0]["text"]
    assert "m:scanned_files=12" in text
    assert "m:indexed_files=7" in text
    assert "m:symbols_extracted=19" in text
    assert "m:errors=1" in text

def test_execute_search_pack_error_sets_is_error_flag(monkeypatch):
    db = MagicMock()
    logger = MagicMock()
    roots = ["/tmp/ws"]
    monkeypatch.setenv("SARI_FORMAT", "pack")
    # Empty query should trigger error
    resp = execute_search({"query": "", "search_type": "code"}, db, logger, roots)
    assert resp.get("isError") is True

def test_execute_search_tolerates_non_mapping_meta():
    db = MagicMock()
    logger = MagicMock()
    roots = ["/tmp/ws"]
    hit = SearchHit(repo="repo1", path="path1", score=1.0, snippet="hi")
    # v3 normalization handles empty meta gracefully
    db.search.return_value = ([hit], None)

    resp = execute_search({"query": "test", "limit": 10, "search_type": "code"}, db, logger, roots)
    text = resp["content"][0]["text"]
    assert "PACK1 tool=search ok=true" in text
    assert "r:t=code p=path1" in text
