"""
Kodara — Core smoke tests.

Tests cover the critical path:
  scanner → graph → memory → context engine → semantic search

Run: pytest tests/
"""

import time
import tempfile
import textwrap
from pathlib import Path

import pytest

from kodara.memory.store import MemoryStore, KodaraIndex, INDEX_VERSION
from kodara.indexer.scanner import RepositoryScanner, FreePlanLimitError, FREE_PLAN_LIMIT
from kodara.search.semantic import SemanticScorer, tokenize, stem
from kodara.graph.dependency import DependencyGraph
from kodara.context_engine.engine import ContextEngine


# ── Fixtures ───────────────────────────────────────────────────────────────────

@pytest.fixture
def sample_project(tmp_path):
    """Create a minimal Python project for testing."""
    (tmp_path / "api").mkdir()
    (tmp_path / "auth").mkdir()
    (tmp_path / "models").mkdir()

    (tmp_path / "main.py").write_text(textwrap.dedent("""
        from api.server import app
        from auth.middleware import verify_token

        def main():
            app.run()
    """))

    (tmp_path / "api" / "server.py").write_text(textwrap.dedent("""
        from auth.middleware import verify_token
        from models.user import User

        class APIServer:
            def run(self): pass
            def handle_request(self): pass
    """))

    (tmp_path / "auth" / "middleware.py").write_text(textwrap.dedent("""
        from models.user import User

        class AuthMiddleware:
            pass

        def verify_token(token: str) -> bool:
            return True
    """))

    (tmp_path / "models" / "user.py").write_text(textwrap.dedent("""
        class User:
            def __init__(self, id: int, email: str):
                self.id = id
                self.email = email

        class UserSession:
            pass
    """))

    return tmp_path


@pytest.fixture
def sample_index(sample_project):
    """Build and return a KodaraIndex from sample_project."""
    scanner = RepositoryScanner(sample_project)
    scan = scanner.scan()

    graph = DependencyGraph()
    graph.build(scan)
    graph_data = graph.to_dict()

    modules = {}
    for path, info in scan.files.items():
        modules[path] = {
            "path": path,
            "module_name": Path(path).stem,
            "language": info.language,
            "classes": info.classes,
            "functions": info.functions,
            "exports": info.exports,
            "imports": info.imports,
            "file_hash": info.hash,
            "summary": "",
            "dependencies": graph.get_dependencies(path),
            "dependents": graph.get_dependents(path),
        }

    return KodaraIndex(
        repo_id=MemoryStore.repo_id_from_path(sample_project),
        display_name="test-project",
        source_root=str(sample_project),
        indexed_at=time.time(),
        index_version=INDEX_VERSION,
        file_count=len(scan.files),
        language_counts=scan.languages,
        modules=modules,
        graph_edges=graph_data["edges"],
        file_hashes={p: f.hash for p, f in scan.files.items()},
    )


# ── 1. Scanner ─────────────────────────────────────────────────────────────────

class TestScanner:

    def test_scan_detects_python_files(self, sample_project):
        scanner = RepositoryScanner(sample_project)
        result = scanner.scan()
        assert len(result.files) > 0
        assert "python" in result.languages

    def test_scan_extracts_classes(self, sample_project):
        scanner = RepositoryScanner(sample_project)
        result = scanner.scan()
        all_classes = [c for f in result.files.values() for c in f.classes]
        assert "User" in all_classes
        assert "AuthMiddleware" in all_classes

    def test_scan_extracts_functions(self, sample_project):
        scanner = RepositoryScanner(sample_project)
        result = scanner.scan()
        all_functions = [fn for f in result.files.values() for fn in f.functions]
        assert "verify_token" in all_functions
        assert "main" in all_functions

    def test_scan_computes_hashes(self, sample_project):
        scanner = RepositoryScanner(sample_project)
        result = scanner.scan()
        for info in result.files.values():
            assert len(info.hash) == 64  # SHA-256 hex

    def test_scan_skips_pycache(self, sample_project):
        pycache = sample_project / "__pycache__"
        pycache.mkdir()
        (pycache / "dummy.pyc").write_bytes(b"")
        scanner = RepositoryScanner(sample_project)
        result = scanner.scan()
        assert not any("__pycache__" in p for p in result.files)

    def test_free_plan_limit(self, tmp_path):
        """FreePlanLimitError raised when files exceed FREE_PLAN_LIMIT."""
        import kodara.indexer.scanner as s
        original = s.FREE_PLAN_LIMIT
        s.FREE_PLAN_LIMIT = 2  # artificially low
        try:
            for i in range(5):
                (tmp_path / f"module_{i}.py").write_text(f"def func_{i}(): pass")
            scanner = RepositoryScanner(tmp_path)
            with pytest.raises(FreePlanLimitError) as exc_info:
                scanner.scan()
            assert exc_info.value.file_count > 2
        finally:
            s.FREE_PLAN_LIMIT = original


# ── 2. Memory Store ────────────────────────────────────────────────────────────

class TestMemoryStore:

    def test_save_and_load(self, sample_index, tmp_path):
        store = MemoryStore(tmp_path)
        store.save(sample_index)
        loaded = store.load()
        assert loaded is not None
        assert loaded.repo_id == sample_index.repo_id
        assert loaded.file_count == sample_index.file_count

    def test_modules_preserved(self, sample_index, tmp_path):
        store = MemoryStore(tmp_path)
        store.save(sample_index)
        loaded = store.load()
        assert len(loaded.modules) == len(sample_index.modules)

    def test_graph_edges_preserved(self, sample_index, tmp_path):
        store = MemoryStore(tmp_path)
        store.save(sample_index)
        loaded = store.load()
        assert len(loaded.graph_edges) == len(sample_index.graph_edges)

    def test_file_hashes_preserved(self, sample_index, tmp_path):
        store = MemoryStore(tmp_path)
        store.save(sample_index)
        loaded = store.load()
        assert loaded.file_hashes == sample_index.file_hashes

    def test_exists(self, sample_index, tmp_path):
        store = MemoryStore(tmp_path)
        assert not store.exists()
        store.save(sample_index)
        assert store.exists()

    def test_delete(self, sample_index, tmp_path):
        store = MemoryStore(tmp_path)
        store.save(sample_index)
        store.delete()
        assert not store.exists()

    def test_get_module(self, sample_index, tmp_path):
        store = MemoryStore(tmp_path)
        store.save(sample_index)
        path = list(sample_index.modules.keys())[0]
        mod = store.get_module(path)
        assert mod is not None
        assert mod["path"] == path

    def test_get_changed_files(self, sample_index, tmp_path):
        store = MemoryStore(tmp_path)
        store.save(sample_index)

        # Simulate: one file modified, one added
        new_hashes = dict(sample_index.file_hashes)
        first_key = list(new_hashes.keys())[0]
        new_hashes[first_key] = "changed_hash_xyz"
        new_hashes["new_file.py"] = "new_file_hash"

        added, modified, deleted = store.get_changed_files(new_hashes)
        assert "new_file.py" in added
        assert first_key in modified

    def test_load_returns_none_when_missing(self, tmp_path):
        store = MemoryStore(tmp_path)
        assert store.load() is None

    def test_repo_id_format(self, tmp_path):
        repo_id = MemoryStore.repo_id_from_path(tmp_path)
        assert repo_id.startswith("local/")
        assert "-" in repo_id


# ── 3. Dependency Graph ────────────────────────────────────────────────────────

class TestDependencyGraph:

    def test_builds_nodes(self, sample_project):
        scanner = RepositoryScanner(sample_project)
        scan = scanner.scan()
        graph = DependencyGraph()
        graph.build(scan)
        assert graph.graph.number_of_nodes() > 0

    def test_builds_edges(self, sample_project):
        scanner = RepositoryScanner(sample_project)
        scan = scanner.scan()
        graph = DependencyGraph()
        graph.build(scan)
        assert graph.graph.number_of_edges() > 0

    def test_to_dict(self, sample_project):
        scanner = RepositoryScanner(sample_project)
        scan = scanner.scan()
        graph = DependencyGraph()
        graph.build(scan)
        data = graph.to_dict()
        assert "nodes" in data
        assert "edges" in data


# ── 4. Semantic Search ─────────────────────────────────────────────────────────

class TestSemanticSearch:

    def test_tokenize_snake_case(self):
        tokens = tokenize("verify_token")
        assert "verify" in tokens
        assert "token" in tokens

    def test_tokenize_camel_case(self):
        tokens = tokenize("getUserById")
        assert "get" in tokens
        assert "user" in tokens
        assert "by" in tokens

    def test_stem_strips_ing(self):
        assert len(stem("handling")) < len("handling")

    def test_stem_strips_ers(self):
        result = stem("processors")
        assert len(result) < len("processors")

    def test_scorer_finds_relevant_modules(self, sample_index):
        scorer = SemanticScorer(sample_index.modules)
        results = scorer.score("authentication middleware")
        assert len(results) > 0
        paths = [r[1] for r in results]
        assert any("auth" in p or "middleware" in p for p in paths)

    def test_scorer_prefix_matching(self, sample_index):
        """'auth' should match 'authentication' and vice versa."""
        scorer = SemanticScorer(sample_index.modules)
        results = scorer.score("auth token")
        paths = [r[1] for r in results]
        assert any("auth" in p for p in paths)

    def test_scorer_returns_sorted(self, sample_index):
        scorer = SemanticScorer(sample_index.modules)
        results = scorer.score("user model")
        scores = [r[0] for r in results]
        assert scores == sorted(scores, reverse=True)


# ── 5. Context Engine ──────────────────────────────────────────────────────────

class TestContextEngine:

    def test_query_returns_results(self, sample_index, tmp_path):
        store = MemoryStore(tmp_path)
        store.save(sample_index)
        engine = ContextEngine(store)
        result = engine.query("authentication middleware")
        assert result is not None
        assert len(result.modules) > 0

    def test_query_token_estimate(self, sample_index, tmp_path):
        store = MemoryStore(tmp_path)
        store.save(sample_index)
        engine = ContextEngine(store)
        result = engine.query("user model")
        assert result.token_estimate > 0

    def test_query_relevant_files(self, sample_index, tmp_path):
        store = MemoryStore(tmp_path)
        store.save(sample_index)
        engine = ContextEngine(store)
        result = engine.query("user session model")
        assert len(result.relevant_files) > 0
        assert any("user" in f for f in result.relevant_files)

    def test_format_context(self, sample_index, tmp_path):
        store = MemoryStore(tmp_path)
        store.save(sample_index)
        engine = ContextEngine(store)
        result = engine.query("api server")
        formatted = engine.format_context(result)
        assert isinstance(formatted, str)
        assert len(formatted) > 0

    def test_query_no_index_returns_none(self, tmp_path):
        store = MemoryStore(tmp_path)
        engine = ContextEngine(store)
        try:
            result = engine.query("anything")
            assert result is None or result.modules == []
        except (ValueError, Exception):
            pass  # engine raises when no index — acceptable behavior
