from __future__ import annotations

import pytest
from pathlib import Path
from unittest.mock import patch, MagicMock

from leadtech_bmad_mcp.knowledge import (
    _safe_path,
    list_domain_files,
    search_knowledge,
    search_global_docs,
    build_search_index,
    write_search_index,
    load_search_index,
    get_index_path,
    read_knowledge_doc,
    read_knowledge_document,
    _extract_excerpt,
    parse_front_matter,
    LeadtechPaths,
)


# ---------------------------------------------------------------------------
# _safe_path
# ---------------------------------------------------------------------------

def test_safe_path_valid(tmp_path):
    result = _safe_path(tmp_path, "sub", "file.md")
    assert result == (tmp_path / "sub" / "file.md").resolve()


def test_safe_path_traversal_blocked(tmp_path):
    with pytest.raises(ValueError, match="hors base"):
        _safe_path(tmp_path, "..", "secret.md")


def test_safe_path_deep_traversal_blocked(tmp_path):
    with pytest.raises(ValueError, match="hors base"):
        _safe_path(tmp_path, "sub", "../../etc/passwd")


# ---------------------------------------------------------------------------
# list_domain_files
# ---------------------------------------------------------------------------

def _make_knowledge(tmp_path: Path) -> LeadtechPaths:
    knowledge = tmp_path / "knowledge"
    (knowledge / "backend" / "patterns").mkdir(parents=True)
    (knowledge / "backend" / "risques").mkdir(parents=True)
    (knowledge / "backend" / "patterns" / "contracts.md").write_text("zod contract schema")
    (knowledge / "backend" / "patterns" / "auth.md").write_text("jwt token session")
    (knowledge / "backend" / "risques" / "general.md").write_text("never store passwords")
    return LeadtechPaths(
        root=tmp_path,
        knowledge=knowledge,
        capitalisation=tmp_path / "95_a_capitaliser.md",
        projects_conf=tmp_path / "_projects.conf",
    )


def test_list_domain_files_returns_md_files(tmp_path):
    paths = _make_knowledge(tmp_path)
    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        files = list_domain_files("backend", "patterns")
    assert len(files) == 2
    assert all(f.suffix == ".md" for f in files)


def test_list_domain_files_invalid_domain(tmp_path):
    with pytest.raises(ValueError, match="Domaine invalide"):
        list_domain_files("unknown_domain", "patterns")


def test_list_domain_files_invalid_bucket(tmp_path):
    with pytest.raises(ValueError, match="Type invalide"):
        list_domain_files("backend", "unknown_bucket")


def test_list_domain_files_missing_dir_returns_empty(tmp_path):
    paths = _make_knowledge(tmp_path)
    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        files = list_domain_files("frontend", "patterns")  # dir not created
    assert files == []


# ---------------------------------------------------------------------------
# search_knowledge
# ---------------------------------------------------------------------------

def test_search_knowledge_finds_match(tmp_path):
    paths = _make_knowledge(tmp_path)
    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        results = search_knowledge("backend", "zod contract")
    assert len(results) >= 1
    titles = [r["title"] for r in results]
    assert "contracts" in titles


def test_search_knowledge_no_match_returns_empty(tmp_path):
    paths = _make_knowledge(tmp_path)
    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        results = search_knowledge("backend", "xyzzy_not_existing_keyword_abc")
    assert results == []


def test_search_knowledge_sorted_by_score(tmp_path):
    paths = _make_knowledge(tmp_path)
    # contracts.md contains "zod" once, auth.md contains "session" once
    # query "zod" should rank contracts first
    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        results = search_knowledge("backend", "zod")
    assert results[0]["title"] == "contracts"


def test_search_knowledge_respects_max_items(tmp_path):
    paths = _make_knowledge(tmp_path)
    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        results = search_knowledge("backend", "a", max_items=1)
    assert len(results) <= 1


def test_search_knowledge_single_bucket(tmp_path):
    paths = _make_knowledge(tmp_path)
    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        results = search_knowledge("backend", "never", bucket="risques")
    assert all(r["bucket"] == "risques" for r in results)


def test_search_knowledge_uses_front_matter_tags(tmp_path):
    knowledge = tmp_path / "knowledge"
    (knowledge / "backend" / "patterns").mkdir(parents=True)
    (knowledge / "backend" / "patterns" / "nestjs.md").write_text(
        "---\n"
        "title: Backend — Patterns : NestJS\n"
        "tags: [guards, auth]\n"
        "---\n\n"
        "# NestJS\n\n"
        "Texte volontairement sans le mot cle demande.\n",
        encoding="utf-8",
    )
    paths = LeadtechPaths(
        root=tmp_path,
        knowledge=knowledge,
        capitalisation=tmp_path / "95_a_capitaliser.md",
        projects_conf=tmp_path / "_projects.conf",
    )
    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        results = search_knowledge("backend", "guards")
    assert results
    assert results[0]["title"] == "Backend — Patterns : NestJS"


def test_build_search_index_includes_knowledge_and_global_docs(tmp_path):
    paths = _make_knowledge(tmp_path)
    _make_global_docs(tmp_path)
    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        payload = build_search_index()

    assert payload["version"] == 1
    assert any(entry["kind"] == "knowledge" for entry in payload["entries"])
    assert any(entry["kind"] == "global" for entry in payload["entries"])


def test_search_knowledge_uses_index_when_present(tmp_path):
    paths = _make_knowledge(tmp_path)
    _make_global_docs(tmp_path)

    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        write_search_index()

    (paths.knowledge / "backend" / "patterns" / "contracts.md").write_text(
        "contenu remplace sans mot cle",
        encoding="utf-8",
    )

    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        results = search_knowledge("backend", "zod")

    assert results
    assert results[0]["title"] == "contracts"


def test_search_knowledge_falls_back_when_index_missing(tmp_path):
    paths = _make_knowledge(tmp_path)
    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        results = search_knowledge("backend", "zod contract")
        index = load_search_index()

    assert results
    assert index is None


def test_load_search_index_invalid_json_returns_none(tmp_path):
    paths = _make_knowledge(tmp_path)
    get_index_path(paths).write_text("{invalid", encoding="utf-8")

    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        payload = load_search_index()

    assert payload is None


# ---------------------------------------------------------------------------
# read_knowledge_doc
# ---------------------------------------------------------------------------

def test_read_knowledge_doc_returns_content(tmp_path):
    paths = _make_knowledge(tmp_path)
    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        content = read_knowledge_doc("backend", "patterns", "contracts")
    assert "zod" in content


def test_read_knowledge_doc_not_found_raises(tmp_path):
    paths = _make_knowledge(tmp_path)
    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        with pytest.raises(FileNotFoundError):
            read_knowledge_doc("backend", "patterns", "nonexistent")


def test_read_knowledge_doc_traversal_blocked(tmp_path):
    paths = _make_knowledge(tmp_path)
    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        with pytest.raises(ValueError, match="hors base"):
            read_knowledge_doc("backend", "patterns", "../../etc/passwd")


def test_read_knowledge_document_splits_metadata_and_body(tmp_path):
    file_path = tmp_path / "doc.md"
    file_path.write_text(
        "---\n"
        "title: Test Doc\n"
        "tags: [alpha, beta]\n"
        "---\n\n"
        "# Heading\n\n"
        "Contenu principal.\n",
        encoding="utf-8",
    )
    doc = read_knowledge_document(file_path)
    assert doc.metadata["title"] == "Test Doc"
    assert doc.metadata["tags"] == ["alpha", "beta"]
    assert doc.body.startswith("# Heading")


# ---------------------------------------------------------------------------
# _extract_excerpt
# ---------------------------------------------------------------------------

def test_extract_excerpt_centered_on_token():
    content = "début " * 30 + "ZOD CONTRACT ici" + " fin" * 30
    excerpt = _extract_excerpt(content, ["zod"])
    assert "ZOD" in excerpt.upper()


def test_extract_excerpt_fallback_to_start():
    content = "a b c d e f g h i j k"
    excerpt = _extract_excerpt(content, ["xyznotfound"])
    assert excerpt.startswith("a b")


def test_extract_excerpt_adds_ellipsis_when_truncated():
    content = "x " * 300
    excerpt = _extract_excerpt(content, ["x"])
    assert excerpt.endswith("…")


def test_extract_excerpt_length_bounded():
    content = "token " * 200
    excerpt = _extract_excerpt(content, ["token"])
    # EXCERPT_LENGTH=400 + éventuelles ellipses (2 chars) + début tronqué (80 chars avant)
    assert len(excerpt) <= 500


def test_parse_front_matter_returns_body_unchanged_without_header():
    content = "# Heading\n\nPlain body"
    metadata, body = parse_front_matter(content)
    assert metadata == {}
    assert body == content


def test_parse_front_matter_parses_lists_and_scalars():
    content = (
        "---\n"
        "title: Demo\n"
        "tags: [alpha, beta]\n"
        "severity: high\n"
        "enabled: true\n"
        "---\n\n"
        "Body\n"
    )
    metadata, body = parse_front_matter(content)
    assert metadata["title"] == "Demo"
    assert metadata["tags"] == ["alpha", "beta"]
    assert metadata["severity"] == "high"
    assert metadata["enabled"] is True
    assert body == "Body\n"


# ---------------------------------------------------------------------------
# search_global_docs
# ---------------------------------------------------------------------------

def _make_global_docs(tmp_path: Path) -> LeadtechPaths:
    (tmp_path / "40_decisions_et_archi.md").write_text("Ne jamais utiliser SQL Server en LXC Proxmox. Toujours PostgreSQL.")
    (tmp_path / "90_debug_et_postmortem.md").write_text("Bug session: expiresAt manquant causait des sessions fantômes.")
    (tmp_path / "10_conventions_redaction.md").write_text("Ne jamais écrire directement dans knowledge/.")
    return LeadtechPaths(
        root=tmp_path,
        knowledge=tmp_path / "knowledge",
        capitalisation=tmp_path / "95_a_capitaliser.md",
        projects_conf=tmp_path / "_projects.conf",
    )


def test_search_global_docs_finds_postmortem(tmp_path):
    paths = _make_global_docs(tmp_path)
    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        results = search_global_docs("session expiresAt")
    assert len(results) >= 1
    assert results[0]["title"] == "debug"


def test_search_global_docs_finds_architecture(tmp_path):
    paths = _make_global_docs(tmp_path)
    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        results = search_global_docs("PostgreSQL Proxmox")
    assert len(results) >= 1
    assert results[0]["title"] == "architecture"


def test_search_global_docs_no_match(tmp_path):
    paths = _make_global_docs(tmp_path)
    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        results = search_global_docs("xyzzy_not_existing_keyword")
    assert results == []


def test_search_global_docs_includes_excerpt(tmp_path):
    paths = _make_global_docs(tmp_path)
    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        results = search_global_docs("PostgreSQL")
    assert "excerpt" in results[0]
    assert len(results[0]["excerpt"]) > 0


def test_search_global_docs_uses_index_when_present(tmp_path):
    paths = _make_global_docs(tmp_path)
    (paths.knowledge / "backend" / "patterns").mkdir(parents=True)

    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        write_search_index()

    (tmp_path / "40_decisions_et_archi.md").write_text("contenu modifie sans postgres", encoding="utf-8")

    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        results = search_global_docs("PostgreSQL")

    assert results
    assert results[0]["title"] == "architecture"


def test_search_global_docs_missing_file_skipped(tmp_path):
    # Seulement 40_ créé, les deux autres absents
    (tmp_path / "40_decisions_et_archi.md").write_text("PostgreSQL recommandé.")
    paths = LeadtechPaths(
        root=tmp_path,
        knowledge=tmp_path / "knowledge",
        capitalisation=tmp_path / "95_a_capitaliser.md",
        projects_conf=tmp_path / "_projects.conf",
    )
    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        results = search_global_docs("PostgreSQL")
    assert len(results) == 1
    assert results[0]["title"] == "architecture"


def test_search_global_docs_respects_max_items(tmp_path):
    paths = _make_global_docs(tmp_path)
    with patch("leadtech_bmad_mcp.knowledge.get_paths", return_value=paths):
        # "dans" apparaît dans les 3 fichiers
        results = search_global_docs("dans jamais", max_items=1)
    assert len(results) <= 1
