from __future__ import annotations

"""
Tests des gates de validate_plan et validate_patch.
On importe les fonctions directement sans démarrer le serveur MCP.
"""

import re
import sys
import types
import pytest
from pathlib import Path
from unittest.mock import patch, MagicMock


def _mock_mcp_module():
    """Injecte un faux module mcp pour éviter l'import de la dépendance."""
    class FastMCP:
        def __init__(self, **kwargs): pass
        def tool(self, description="", **k): return lambda f: f
        def resource(self, path, **k): return lambda f: f
        def run(self): pass

    mcp_mod = types.ModuleType("mcp")
    mcp_server = types.ModuleType("mcp.server")
    mcp_fastmcp = types.ModuleType("mcp.server.fastmcp")
    mcp_fastmcp.FastMCP = FastMCP
    sys.modules.setdefault("mcp", mcp_mod)
    sys.modules.setdefault("mcp.server", mcp_server)
    sys.modules.setdefault("mcp.server.fastmcp", mcp_fastmcp)


_mock_mcp_module()

from leadtech_bmad_mcp.server import get_guidance, validate_plan, validate_patch  # noqa: E402
from leadtech_bmad_mcp.knowledge import LeadtechPaths  # noqa: E402


def _fake_paths(tmp_path: Path) -> LeadtechPaths:
    (tmp_path / "80_bmad").mkdir(parents=True, exist_ok=True)
    (tmp_path / "knowledge" / "workflow" / "risques").mkdir(parents=True, exist_ok=True)
    (tmp_path / "80_bmad" / "process_llm_et_parallelisation.md").write_text("")
    (tmp_path / "knowledge" / "workflow" / "risques" / "story-tracking.md").write_text("")
    return LeadtechPaths(
        root=tmp_path,
        knowledge=tmp_path / "knowledge",
        capitalisation=tmp_path / "95_a_capitaliser.md",
        projects_conf=tmp_path / "_projects.conf",
    )


# ---------------------------------------------------------------------------
# validate_plan — contracts gate
# ---------------------------------------------------------------------------

class TestValidatePlanContracts:
    def test_blocks_when_no_contract_reference(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_plan("backend", "On va implémenter un service utilisateur.", strict=True)
        assert result["blocking_issues"]
        assert any("contrats" in i.lower() for i in result["blocking_issues"])

    def test_passes_with_zod(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_plan("backend", "On utilise Zod pour valider les inputs. Tests unitaires inclus.", strict=True)
        assert not result["blocking_issues"]

    def test_passes_with_z_object(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_plan("backend", "Schema: z.object({ id: z.string() }). Unit tests inclus.", strict=True)
        assert not result["blocking_issues"]

    def test_passes_with_contracts_first(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_plan("backend", "Approche contracts-first avec shared_contract. Unit tests inclus.", strict=True)
        assert not result["blocking_issues"]

    def test_no_block_on_frontend(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_plan("frontend", "On construit un formulaire React. Unit tests inclus.", strict=True)
        # Pas de blocking sur frontend pour l'absence de Zod
        contract_blocks = [i for i in result["blocking_issues"] if "contrats" in i.lower()]
        assert not contract_blocks


class TestGetGuidanceMetadata:
    def test_exposes_matched_docs_with_metadata(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch(
            "leadtech_bmad_mcp.server.search_knowledge",
            return_value=[
                {
                    "path": str(tmp_path / "knowledge" / "backend" / "patterns" / "nestjs.md"),
                    "bucket": "patterns",
                    "title": "Backend — Patterns : NestJS",
                    "score": "9",
                    "excerpt": "Pattern sur les guards globaux.",
                    "severity": "medium",
                    "applies_to": "analysis, implementation, review",
                    "tags": "nestjs, guards, auth",
                }
            ],
        ), patch(
            "leadtech_bmad_mcp.server.search_global_docs",
            return_value=[],
        ), patch(
            "leadtech_bmad_mcp.server.get_paths",
            return_value=paths,
        ):
            result = get_guidance("backend", "implementation", story_text="Ajouter un guard NestJS")

        assert result["matched_docs"]
        matched = result["matched_docs"][0]
        assert matched["bucket"] == "patterns"
        assert matched["severity"] == "medium"
        assert "implementation" in matched["applies_to"]
        assert matched["read_uri"] == "leadtech://knowledge/backend/patterns/nestjs"
        assert any("severity=medium" in item for item in result["must_do"])


class TestValidatePlanRequestId:
    def test_suggests_requestid_when_error_without_requestid(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_plan("backend", "On gère les erreurs HTTP. Zod schema. Tests unitaires.", strict=False)
        assert any("requestid" in s.lower() or "requestId" in s for s in result["should_do"])

    def test_no_suggestion_when_requestid_present(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_plan("backend", "On retourne { error: { code, message, requestId } }. Zod. Tests.", strict=False)
        assert not any("requestid" in s.lower() for s in result["should_do"])

    def test_requestid_snake_case_accepted(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_plan("backend", "Le champ request_id est dans chaque error. Zod. Tests.", strict=False)
        assert not any("requestid" in s.lower() for s in result["should_do"])


class TestValidatePlanTests:
    def test_flags_missing_test_strategy(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_plan("backend", "On implémente la feature X avec Zod contracts-first.", strict=False)
        assert any("test" in s.lower() for s in result["must_do"])

    def test_no_flag_when_tests_mentioned(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_plan("backend", "Feature X avec Zod. Unit tests et integration tests inclus.", strict=False)
        assert not any("strategie de test" in s.lower() for s in result["must_do"])

    def test_spec_keyword_accepted(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_plan("backend", "Feature X avec Zod. Spec files inclus pour chaque module.", strict=False)
        assert not any("strategie de test" in s.lower() for s in result["must_do"])


class TestValidatePlanParallel:
    def test_flags_parallel_without_depends_on(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_plan("backend", "Cette story est parallel-safe. Zod. Tests.", strict=False)
        assert any("parallel" in s.lower() for s in result["red_flags"])

    def test_no_flag_when_depends_on_present(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_plan("backend", "parallel-safe: true. depends-on: story-1. Zod. Tests.", strict=False)
        assert not any("parallel" in s.lower() for s in result["red_flags"])


# ---------------------------------------------------------------------------
# validate_patch — backend gates
# ---------------------------------------------------------------------------

class TestValidatePatchSession:
    def test_blocks_session_without_expires_at(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_patch("backend", "UPDATE sessions SET user_id = ? WHERE id = ?")
        assert any("expiresat" in i.lower() or "expiresat" in i.lower() for i in result["blocking_issues"])

    def test_passes_session_with_expires_at_camel(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_patch("backend", "UPDATE sessions SET user_id = ?, expiresAt = ? WHERE id = ?")
        session_blocks = [i for i in result["blocking_issues"] if "expiresat" in i.lower() or "session" in i.lower()]
        assert not session_blocks

    def test_passes_session_with_expires_at_snake(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_patch("backend", "ALTER TABLE sessions ADD COLUMN expires_at TIMESTAMP;")
        session_blocks = [i for i in result["blocking_issues"] if "expiresat" in i.lower() or "session" in i.lower()]
        assert not session_blocks


class TestValidatePatchRequestId:
    def test_flags_error_without_requestid(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_patch("backend", "throw new Error('not found')")
        assert any("requestid" in s.lower() for s in result["must_do"])

    def test_no_flag_with_requestid_camel(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_patch("backend", "return { error: { code: 'NOT_FOUND', message, requestId: uuid() } }")
        assert not any("requestid" in s.lower() for s in result["must_do"])

    def test_no_flag_with_request_id_snake(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_patch("backend", "error_response = { code: 'ERR', request_id: generate() }")
        assert not any("requestid" in s.lower() for s in result["must_do"])


class TestValidatePatchAuthGuard:
    def test_flags_guard_without_authguard(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_patch("backend", "@UseGuards(RolesGuard)\nasync someMethod() {}")
        assert any("authguard" in s.lower() or "auth" in s.lower() for s in result["red_flags"])

    def test_no_flag_with_authguard(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_patch("backend", "@UseGuards(AuthGuard('jwt'), RolesGuard)\nasync someMethod() {}")
        assert not any("authguard" in s.lower() for s in result["red_flags"])

    def test_no_flag_with_jwtauthguard(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_patch("backend", "@UseGuards(JwtAuthGuard, RolesGuard)\nasync someMethod() {}")
        assert not any("authguard" in s.lower() for s in result["red_flags"])


class TestValidatePatchTests:
    def test_suggests_tests_when_no_spec_file(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_patch("backend", "const x = 1;", strict=True)
        assert any("test" in s.lower() for s in result["should_do"])

    def test_no_suggestion_when_describe_block(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_patch("backend", "describe('x', () => { it('works', () => {}) })", strict=True)
        assert not any("test" in s.lower() for s in result["should_do"])

    def test_no_suggestion_when_it_block(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_patch("backend", "it('does something', () => { expect(x).toBe(1) })", strict=True)
        assert not any("test" in s.lower() for s in result["should_do"])


class TestValidatePatchBmadOnly:
    def test_blocks_when_only_bmad_artefacts(self, tmp_path):
        paths = _fake_paths(tmp_path)
        with patch("leadtech_bmad_mcp.server.get_paths", return_value=paths):
            result = validate_patch(
                "backend",
                "story update",
                changed_files=["_bmad-output/story-1.md", "_bmad-output/sprint-status.yaml"],
            )
        assert any("artefact" in i.lower() or "bmad" in i.lower() for i in result["blocking_issues"])
