"""Tests for glreview.analyze module."""

from __future__ import annotations

from pathlib import Path

import pytest

from glreview.analyze import (
    ClassInfo,
    FunctionInfo,
    ModuleSummary,
    analyze_module,
    analyze_python_file,
)


class TestFunctionInfo:
    """Tests for FunctionInfo dataclass."""

    def test_default_values(self):
        """Default values are correct."""
        func = FunctionInfo(name="test", lineno=10)
        assert func.name == "test"
        assert func.lineno == 10
        assert func.is_method is False
        assert func.is_private is False

    def test_display_name(self):
        """display_name returns the name."""
        func = FunctionInfo(name="test_func", lineno=10)
        assert func.display_name == "test_func"


class TestClassInfo:
    """Tests for ClassInfo dataclass."""

    def test_default_values(self):
        """Default values are correct."""
        cls = ClassInfo(name="TestClass", lineno=5)
        assert cls.name == "TestClass"
        assert cls.lineno == 5
        assert cls.methods == []
        assert cls.is_private is False

    def test_public_methods_excludes_private(self):
        """public_methods excludes underscore methods."""
        cls = ClassInfo(
            name="Test",
            lineno=1,
            methods=[
                FunctionInfo(name="public", lineno=2),
                FunctionInfo(name="_private", lineno=3),
                FunctionInfo(name="__init__", lineno=4),
                FunctionInfo(name="another_public", lineno=5),
            ],
        )
        public = cls.public_methods
        names = [m.name for m in public]
        assert "public" in names
        assert "another_public" in names
        assert "__init__" in names  # dunder methods included
        assert "_private" not in names


class TestModuleSummary:
    """Tests for ModuleSummary dataclass."""

    def test_public_classes(self):
        """public_classes excludes private classes."""
        summary = ModuleSummary(
            classes=[
                ClassInfo(name="Public", lineno=1),
                ClassInfo(name="_Private", lineno=10),
                ClassInfo(name="AnotherPublic", lineno=20),
            ]
        )
        public = summary.public_classes
        names = [c.name for c in public]
        assert "Public" in names
        assert "AnotherPublic" in names
        assert "_Private" not in names

    def test_public_functions(self):
        """public_functions excludes private functions."""
        summary = ModuleSummary(
            functions=[
                FunctionInfo(name="public_func", lineno=1),
                FunctionInfo(name="_private_func", lineno=10),
                FunctionInfo(name="another_public", lineno=20),
            ]
        )
        public = summary.public_functions
        names = [f.name for f in public]
        assert "public_func" in names
        assert "another_public" in names
        assert "_private_func" not in names

    def test_format_summary_with_parse_error(self):
        """format_summary shows parse error."""
        summary = ModuleSummary(parse_error="Syntax error")
        result = summary.format_summary()
        assert "Could not parse" in result
        assert "Syntax error" in result

    def test_format_summary_empty(self):
        """format_summary for empty module."""
        summary = ModuleSummary()
        result = summary.format_summary()
        assert "No public classes or functions" in result

    def test_format_summary_with_classes(self):
        """format_summary includes classes."""
        summary = ModuleSummary(
            classes=[
                ClassInfo(name="Foo", lineno=1),
                ClassInfo(name="Bar", lineno=10),
            ]
        )
        result = summary.format_summary()
        assert "**Classes:**" in result
        assert "`Foo`" in result
        assert "`Bar`" in result

    def test_format_summary_with_functions(self):
        """format_summary includes functions."""
        summary = ModuleSummary(
            functions=[
                FunctionInfo(name="func_a", lineno=1),
                FunctionInfo(name="func_b", lineno=10),
            ]
        )
        result = summary.format_summary()
        assert "**Functions:**" in result
        assert "`func_a`" in result
        assert "`func_b`" in result

    def test_format_summary_truncates(self):
        """format_summary truncates long lists."""
        summary = ModuleSummary(
            classes=[ClassInfo(name=f"Class{i}", lineno=i) for i in range(12)]
        )
        result = summary.format_summary(max_items=5)
        assert "`Class0`" in result
        assert "+7 more" in result

    def test_format_summary_truncates_functions(self):
        """format_summary truncates long function lists."""
        summary = ModuleSummary(
            functions=[FunctionInfo(name=f"func{i}", lineno=i) for i in range(12)]
        )
        result = summary.format_summary(max_items=5)
        assert "`func0`" in result
        assert "+7 more" in result


class TestAnalyzePythonFile:
    """Tests for analyze_python_file function."""

    def test_analyzes_classes(self, temp_dir):
        """Extracts class information."""
        code = '''
class MyClass:
    """A test class."""

    def method(self):
        pass

    def _private_method(self):
        pass
'''
        path = temp_dir / "test.py"
        path.write_text(code)

        summary = analyze_python_file(path)

        assert len(summary.classes) == 1
        assert summary.classes[0].name == "MyClass"
        assert len(summary.classes[0].methods) == 2

    def test_analyzes_functions(self, temp_dir):
        """Extracts function information."""
        code = '''
def public_func():
    pass

def _private_func():
    pass

async def async_func():
    pass
'''
        path = temp_dir / "test.py"
        path.write_text(code)

        summary = analyze_python_file(path)

        assert len(summary.functions) == 3
        names = [f.name for f in summary.functions]
        assert "public_func" in names
        assert "_private_func" in names
        assert "async_func" in names

    def test_analyzes_imports(self, temp_dir):
        """Extracts import information."""
        code = '''
import os
import sys
from pathlib import Path
from typing import TYPE_CHECKING
'''
        path = temp_dir / "test.py"
        path.write_text(code)

        summary = analyze_python_file(path)

        assert "os" in summary.imports
        assert "sys" in summary.imports
        assert "pathlib" in summary.imports
        assert "typing" in summary.imports

    def test_handles_syntax_error(self, temp_dir):
        """Handles files with syntax errors."""
        code = '''
def broken(
    # Missing closing paren
'''
        path = temp_dir / "test.py"
        path.write_text(code)

        summary = analyze_python_file(path)

        assert summary.parse_error is not None
        assert "Syntax error" in summary.parse_error

    def test_handles_general_exception(self, temp_dir):
        """Handles files that cause general exceptions."""
        # Create a file with binary content that can't be decoded
        path = temp_dir / "test.py"
        path.write_bytes(b"\xff\xfe invalid utf-8 \x80\x81")

        summary = analyze_python_file(path)

        assert summary.parse_error is not None

    def test_handles_async_methods(self, temp_dir):
        """Extracts async methods."""
        code = '''
class AsyncClass:
    async def async_method(self):
        pass
'''
        path = temp_dir / "test.py"
        path.write_text(code)

        summary = analyze_python_file(path)

        assert len(summary.classes) == 1
        assert len(summary.classes[0].methods) == 1
        assert summary.classes[0].methods[0].name == "async_method"

    def test_private_class(self, temp_dir):
        """Marks private classes."""
        code = '''
class _PrivateClass:
    pass
'''
        path = temp_dir / "test.py"
        path.write_text(code)

        summary = analyze_python_file(path)

        assert summary.classes[0].is_private is True


class TestAnalyzeModule:
    """Tests for analyze_module function."""

    def test_analyzes_module(self, temp_dir):
        """Analyzes module by relative path."""
        src = temp_dir / "src"
        src.mkdir()
        code = '''
def hello():
    pass
'''
        (src / "module.py").write_text(code)

        summary = analyze_module("src/module.py", cwd=temp_dir)

        assert summary.parse_error is None
        assert len(summary.functions) == 1

    def test_file_not_found(self, temp_dir):
        """Returns error for missing file."""
        summary = analyze_module("nonexistent.py", cwd=temp_dir)
        assert summary.parse_error == "File not found"

    def test_not_python_file(self, temp_dir):
        """Returns error for non-Python file."""
        (temp_dir / "file.txt").write_text("hello")
        summary = analyze_module("file.txt", cwd=temp_dir)
        assert summary.parse_error == "Not a Python file"

    def test_defaults_to_cwd(self, temp_dir, monkeypatch):
        """Uses cwd when not specified."""
        monkeypatch.chdir(temp_dir)
        code = "def func(): pass"
        (temp_dir / "module.py").write_text(code)

        summary = analyze_module("module.py")

        assert summary.parse_error is None
        assert len(summary.functions) == 1
