"""Tests for brynq.auto_documenter — Phase 63 auto-documentation."""

import json
import pytest
from pathlib import Path


@pytest.fixture(autouse=True)
def _isolate_docs(tmp_path, monkeypatch):
    """Redirect docs storage to temp directory."""
    docs_dir = tmp_path / "docs"
    docs_dir.mkdir()
    monkeypatch.setattr("brynq.auto_documenter.DOCS_DIR", docs_dir)


@pytest.fixture
def sample_module(tmp_path):
    """Create a sample Python module."""
    p = tmp_path / "sample.py"
    p.write_text('''\
"""Sample module docstring."""

def hello(name):
    """Say hello to someone."""
    return f"Hello, {name}"

def _private():
    pass

def undocumented():
    pass

class MyClass:
    """A documented class."""

    def method_a(self, x, y):
        """Do something with x and y."""
        return x + y

    def method_b(self):
        pass

    def _internal(self):
        """Internal helper."""
        pass
''', encoding="utf-8")
    return str(p)


@pytest.fixture
def undocumented_module(tmp_path):
    """Create a module with no docstrings."""
    p = tmp_path / "bare.py"
    p.write_text('''\
def foo():
    pass

def bar(x):
    pass

class Empty:
    def run(self):
        pass
''', encoding="utf-8")
    return str(p)


@pytest.fixture
def src_dir(tmp_path, sample_module, undocumented_module):
    """Create a source directory with multiple modules."""
    src = tmp_path / "src"
    src.mkdir()
    (src / "mod_a.py").write_text('"""Module A."""\ndef func_a():\n    """Documented."""\n    pass\n')
    (src / "mod_b.py").write_text('def func_b():\n    pass\n')
    (src / "__init__.py").write_text('')
    return str(src)


# ── _extract_from_source ──────────────────────────────────────────────────


class TestExtract:
    def test_extracts_functions(self):
        from brynq.auto_documenter import _extract_from_source
        result = _extract_from_source('def foo():\n    """Doc."""\n    pass\n', "test.py")
        assert len(result["functions"]) == 1
        assert result["functions"][0]["name"] == "foo"
        assert result["functions"][0]["docstring"] == "Doc."

    def test_extracts_classes(self):
        from brynq.auto_documenter import _extract_from_source
        result = _extract_from_source('class Foo:\n    """A class."""\n    pass\n', "t.py")
        assert len(result["classes"]) == 1
        assert result["classes"][0]["docstring"] == "A class."

    def test_extracts_methods(self):
        from brynq.auto_documenter import _extract_from_source
        src = 'class C:\n    def m(self, x):\n        """Method."""\n        pass\n'
        result = _extract_from_source(src, "t.py")
        assert len(result["classes"][0]["methods"]) == 1
        assert result["classes"][0]["methods"][0]["args"] == ["x"]

    def test_syntax_error(self):
        from brynq.auto_documenter import _extract_from_source
        result = _extract_from_source("def broken(:", "t.py")
        assert "error" in result

    def test_module_docstring(self):
        from brynq.auto_documenter import _extract_from_source
        result = _extract_from_source('"""Module doc."""\ndef f(): pass\n', "t.py")
        assert result["module_docstring"] == "Module doc."

    def test_private_function_marked(self):
        from brynq.auto_documenter import _extract_from_source
        result = _extract_from_source('def _helper(): pass\n', "t.py")
        assert result["functions"][0]["is_private"] is True


# ── document_module ───────────────────────────────────────────────────────


class TestDocumentModule:
    def test_returns_entry(self, sample_module):
        from brynq.auto_documenter import document_module
        entry = document_module(sample_module)
        assert entry["module_name"] == "sample"
        assert entry["total_functions"] >= 3
        assert entry["total_classes"] == 1

    def test_creates_file(self, sample_module):
        from brynq.auto_documenter import document_module, DOCS_DIR
        document_module(sample_module)
        assert (DOCS_DIR / "sample.json").exists()

    def test_file_not_found(self):
        from brynq.auto_documenter import document_module
        with pytest.raises(FileNotFoundError):
            document_module("/nonexistent/module.py")

    def test_not_python(self, tmp_path):
        from brynq.auto_documenter import document_module
        p = tmp_path / "file.txt"
        p.write_text("hello")
        with pytest.raises(ValueError, match="Not a Python"):
            document_module(str(p))

    def test_has_documented_at(self, sample_module):
        from brynq.auto_documenter import document_module
        entry = document_module(sample_module)
        assert "documented_at" in entry


# ── document_all ──────────────────────────────────────────────────────────


class TestDocumentAll:
    def test_documents_directory(self, src_dir):
        from brynq.auto_documenter import document_all
        result = document_all(src_dir)
        assert result["modules_documented"] == 2  # mod_a, mod_b (not __init__)

    def test_invalid_dir(self):
        from brynq.auto_documenter import document_all
        with pytest.raises(ValueError, match="Not a directory"):
            document_all("/nonexistent/dir")

    def test_returns_module_names(self, src_dir):
        from brynq.auto_documenter import document_all
        result = document_all(src_dir)
        assert "mod_a" in result["modules"]


# ── get_documentation ─────────────────────────────────────────────────────


class TestGetDocumentation:
    def test_found(self, sample_module):
        from brynq.auto_documenter import document_module, get_documentation
        document_module(sample_module)
        doc = get_documentation("sample")
        assert doc is not None
        assert doc["module_name"] == "sample"

    def test_not_found(self):
        from brynq.auto_documenter import get_documentation
        assert get_documentation("no_such_module") is None


# ── get_undocumented ──────────────────────────────────────────────────────


class TestGetUndocumented:
    def test_finds_missing_docstrings(self, sample_module):
        from brynq.auto_documenter import document_module, get_undocumented
        document_module(sample_module)
        missing = get_undocumented()
        names = [m["name"] for m in missing]
        assert "undocumented" in names

    def test_finds_undocumented_methods(self, sample_module):
        from brynq.auto_documenter import document_module, get_undocumented
        document_module(sample_module)
        missing = get_undocumented()
        names = [m["name"] for m in missing]
        assert "MyClass.method_b" in names

    def test_skips_private_methods(self, sample_module):
        from brynq.auto_documenter import document_module, get_undocumented
        document_module(sample_module)
        missing = get_undocumented()
        names = [m["name"] for m in missing]
        # _internal has a docstring so it wouldn't appear anyway
        # But _private (function) should not appear as undocumented method
        assert "MyClass._internal" not in names

    def test_bare_module(self, undocumented_module):
        from brynq.auto_documenter import document_module, get_undocumented
        document_module(undocumented_module)
        missing = get_undocumented()
        assert len(missing) >= 3  # module, foo, bar, Empty, run


# ── get_doc_coverage ──────────────────────────────────────────────────────


class TestGetDocCoverage:
    def test_fully_documented(self, tmp_path):
        from brynq.auto_documenter import document_module, get_doc_coverage
        p = tmp_path / "full.py"
        p.write_text('"""Mod."""\ndef f():\n    """Fn."""\n    pass\n')
        document_module(str(p))
        cov = get_doc_coverage()
        assert cov["coverage_percent"] == 100.0

    def test_partial_coverage(self, sample_module):
        from brynq.auto_documenter import document_module, get_doc_coverage
        document_module(sample_module)
        cov = get_doc_coverage()
        assert 0 < cov["coverage_percent"] < 100

    def test_zero_coverage(self, undocumented_module):
        from brynq.auto_documenter import document_module, get_doc_coverage
        document_module(undocumented_module)
        cov = get_doc_coverage()
        assert cov["coverage_percent"] < 50
        assert cov["undocumented_items"] > 0

    def test_empty(self):
        from brynq.auto_documenter import get_doc_coverage
        cov = get_doc_coverage()
        assert cov["modules_scanned"] == 0
        assert cov["coverage_percent"] == 0.0
