"""Tests for MCP Code Quality Analyzer."""

from __future__ import annotations

import json
import textwrap

import pytest

from mcpaudit.code_quality import (
    DeadCodePattern,
    DuplicateBlock,
    FunctionMetrics,
    QualityIssue,
    QualityLevel,
    QualityReport,
    _count_branches,
    _count_lines,
    _detect_dead_code,
    _detect_duplicates,
    _max_nesting_depth,
    _count_returns,
    _calculate_quality_score,
    analyze_code_quality,
    format_quality_json,
    format_quality_text,
)
import ast


# --- QualityLevel ---

class TestQualityLevel:
    def test_from_score_excellent(self):
        assert QualityLevel.from_score(95) == QualityLevel.EXCELLENT

    def test_from_score_good(self):
        assert QualityLevel.from_score(75) == QualityLevel.GOOD

    def test_from_score_fair(self):
        assert QualityLevel.from_score(55) == QualityLevel.FAIR

    def test_from_score_poor(self):
        assert QualityLevel.from_score(35) == QualityLevel.POOR

    def test_from_score_critical(self):
        assert QualityLevel.from_score(15) == QualityLevel.CRITICAL

    def test_boundary_90(self):
        assert QualityLevel.from_score(90) == QualityLevel.EXCELLENT

    def test_boundary_70(self):
        assert QualityLevel.from_score(70) == QualityLevel.GOOD

    def test_boundary_50(self):
        assert QualityLevel.from_score(50) == QualityLevel.FAIR

    def test_boundary_30(self):
        assert QualityLevel.from_score(30) == QualityLevel.POOR

    def test_boundary_0(self):
        assert QualityLevel.from_score(0) == QualityLevel.CRITICAL


# --- FunctionMetrics ---

class TestFunctionMetrics:
    def test_no_issues(self):
        fm = FunctionMetrics(name="f", line=1, length=10, cyclomatic_complexity=2)
        assert fm.issues == []

    def test_too_long(self):
        fm = FunctionMetrics(name="f", line=1, length=150, is_too_long=True)
        assert any("Too long" in i for i in fm.issues)

    def test_too_complex(self):
        fm = FunctionMetrics(name="f", line=1, cyclomatic_complexity=15, is_too_complex=True)
        assert any("Too complex" in i for i in fm.issues)

    def test_deep_nesting(self):
        fm = FunctionMetrics(name="f", line=1, nesting_depth=6)
        assert any("nesting" in i.lower() for i in fm.issues)

    def test_too_many_params(self):
        fm = FunctionMetrics(name="f", line=1, parameter_count=8)
        assert any("param" in i.lower() for i in fm.issues)

    def test_god_function(self):
        fm = FunctionMetrics(name="f", line=1, is_god_function=True)
        assert any("God" in i for i in fm.issues)

    def test_to_dict(self):
        fm = FunctionMetrics(name="test", line=5, length=20, cyclomatic_complexity=3)
        d = fm.to_dict()
        assert d["name"] == "test"
        assert d["length"] == 20


# --- QualityReport ---

class TestQualityReport:
    def test_empty_report(self):
        r = QualityReport()
        assert r.quality_score == 100.0
        assert r.issue_counts == {}

    def test_issue_counts(self):
        r = QualityReport(issues=[
            QualityIssue(line=1, category="x", severity="HIGH", message="m"),
            QualityIssue(line=2, category="y", severity="HIGH", message="m"),
            QualityIssue(line=3, category="z", severity="LOW", message="m"),
        ])
        assert r.issue_counts["HIGH"] == 2
        assert r.issue_counts["LOW"] == 1

    def test_to_dict(self):
        r = QualityReport(total_lines=100, quality_score=85.0)
        d = r.to_dict()
        assert d["total_lines"] == 100
        assert d["quality_score"] == 85.0


# --- _count_lines ---

class TestCountLines:
    def test_empty(self):
        t, c, co, b = _count_lines("")
        assert t == 1

    def test_code_only(self):
        t, c, co, b = _count_lines("x = 1\ny = 2")
        assert c == 2

    def test_comments(self):
        t, c, co, b = _count_lines("# comment\nx = 1")
        assert co >= 1

    def test_blank_lines(self):
        t, c, co, b = _count_lines("x = 1\n\ny = 2")
        assert b >= 1

    def test_docstrings(self):
        code = '"""docstring"""\nx = 1'
        t, c, co, b = _count_lines(code)
        assert co >= 1


# --- AST helpers ---

class TestASTHelpers:
    def test_count_branches_if(self):
        tree = ast.parse("if x: pass")
        assert _count_branches(tree) >= 1

    def test_count_branches_for(self):
        tree = ast.parse("for x in y: pass")
        assert _count_branches(tree) >= 1

    def test_count_branches_while(self):
        tree = ast.parse("while True: pass")
        assert _count_branches(tree) >= 1

    def test_count_branches_boolean(self):
        tree = ast.parse("if a and b: pass")
        assert _count_branches(tree) >= 2

    def test_count_branches_except(self):
        tree = ast.parse("try:\n pass\nexcept: pass")
        assert _count_branches(tree) >= 1

    def test_nesting_depth_flat(self):
        tree = ast.parse("x = 1")
        assert _max_nesting_depth(tree) == 0

    def test_nesting_depth_one(self):
        tree = ast.parse("if True:\n x = 1")
        assert _max_nesting_depth(tree) >= 1

    def test_nesting_depth_nested(self):
        code = "if True:\n if True:\n  if True:\n   x = 1"
        tree = ast.parse(code)
        assert _max_nesting_depth(tree) >= 3

    def test_count_returns(self):
        code = "def f():\n return 1\n return 2"
        tree = ast.parse(code)
        func = tree.body[0]
        assert _count_returns(func) == 2


# --- Dead code detection ---

class TestDeadCode:
    def test_commented_code(self):
        code = "x = 1\n# def old_function():\ny = 2"
        tree = ast.parse(code)
        dc = _detect_dead_code(code, tree)
        assert any(d.pattern_type == "commented_code" for d in dc)

    def test_unreachable_after_return(self):
        code = "def f():\n return 1\n x = 2"
        tree = ast.parse(code)
        dc = _detect_dead_code(code, tree)
        assert any(d.pattern_type == "unreachable" for d in dc)

    def test_unused_import(self):
        code = "import os\nx = 1"
        tree = ast.parse(code)
        dc = _detect_dead_code(code, tree)
        assert any(d.pattern_type == "unused_import" for d in dc)

    def test_no_dead_code(self):
        code = "import os\nprint(os.getcwd())"
        tree = ast.parse(code)
        dc = _detect_dead_code(code, tree)
        unused = [d for d in dc if d.pattern_type == "unused_import"]
        assert len(unused) == 0

    def test_dead_code_to_dict(self):
        dc = DeadCodePattern(line=5, pattern_type="unreachable", description="Unreachable")
        d = dc.to_dict()
        assert d["line"] == 5
        assert d["pattern_type"] == "unreachable"


# --- Duplication detection ---

class TestDuplication:
    def test_no_duplicates_short(self):
        code = "x = 1\ny = 2"
        dups = _detect_duplicates(code)
        assert len(dups) == 0

    def test_duplicate_block_to_dict(self):
        db = DuplicateBlock(lines_a=(1, 5), lines_b=(10, 15), similarity=1.0)
        d = db.to_dict()
        assert d["lines_a"] == [1, 5]


# --- Main analysis ---

class TestAnalyzeCodeQuality:
    def test_empty_code(self):
        r = analyze_code_quality("")
        assert r.quality_score == 100.0
        assert r.quality_level == QualityLevel.EXCELLENT

    def test_simple_code(self):
        code = "def hello():\n    return 'hello'"
        r = analyze_code_quality(code)
        assert r.quality_score > 50
        assert r.function_count == 1

    def test_syntax_error(self):
        code = "def f(\n  broken"
        r = analyze_code_quality(code)
        assert r.quality_score == 0.0
        assert r.quality_level == QualityLevel.CRITICAL

    def test_complex_function(self):
        # Generate a function with high complexity
        lines = ["def complex_func(a, b, c, d, e, f):"]
        for i in range(12):
            indent = "    " * (i % 3 + 1)
            lines.append(f"{indent}if a == {i}:")
            lines.append(f"{indent}    x = {i}")
        code = "\n".join(lines)
        r = analyze_code_quality(code)
        assert r.max_complexity > 5

    def test_function_metrics(self):
        code = textwrap.dedent("""\
        def short_func():
            return 1

        def medium_func(a, b, c):
            if a:
                return b
            return c
        """)
        r = analyze_code_quality(code)
        assert r.function_count == 2

    def test_class_counting(self):
        code = "class A:\n    pass\nclass B:\n    pass"
        r = analyze_code_quality(code)
        assert r.class_count == 2

    def test_long_function_detected(self):
        lines = ["def long_func():"]
        for i in range(110):
            lines.append(f"    x_{i} = {i}")
        code = "\n".join(lines)
        r = analyze_code_quality(code)
        assert any("lines (max 100)" in str(i.message) for i in r.issues)

    def test_deep_nesting_detected(self):
        code = textwrap.dedent("""\
        def nested():
            if True:
                if True:
                    if True:
                        if True:
                            if True:
                                return 1
        """)
        r = analyze_code_quality(code)
        assert r.max_nesting >= 4

    def test_many_params_detected(self):
        code = "def f(a, b, c, d, e, f, g, h):\n    return a"
        r = analyze_code_quality(code)
        assert any("param" in i.message.lower() for i in r.issues)

    def test_line_count_correct(self):
        code = "# comment\n\ndef f():\n    pass\n"
        r = analyze_code_quality(code)
        assert r.total_lines > 0
        assert r.comment_lines >= 1
        assert r.blank_lines >= 1

    def test_quality_score_range(self):
        code = "def f():\n    return 1"
        r = analyze_code_quality(code)
        assert 0 <= r.quality_score <= 100

    def test_report_to_dict(self):
        code = "def f():\n    return 1"
        r = analyze_code_quality(code)
        d = r.to_dict()
        assert "quality_score" in d
        assert "functions" in d

    def test_async_function(self):
        code = "async def handler():\n    return 1"
        r = analyze_code_quality(code)
        assert r.function_count == 1

    def test_long_line_detected(self):
        code = "x = " + "a" * 130
        r = analyze_code_quality(code)
        assert any("chars" in i.message for i in r.issues)

    def test_multiple_return_warning(self):
        code = textwrap.dedent("""\
        def f(x):
            if x == 1: return 1
            if x == 2: return 2
            if x == 3: return 3
            if x == 4: return 4
            if x == 5: return 5
            return 0
        """)
        r = analyze_code_quality(code)
        funcs_with_many_returns = [f for f in r.functions if f.return_count > 5]
        assert len(funcs_with_many_returns) == 1


# --- _calculate_quality_score ---

class TestCalculateQualityScore:
    def test_perfect_score(self):
        r = QualityReport()
        assert _calculate_quality_score(r) == 100.0

    def test_high_issues_deduct(self):
        r = QualityReport(issues=[
            QualityIssue(line=1, category="x", severity="HIGH", message="m")
            for _ in range(5)
        ])
        assert _calculate_quality_score(r) < 100.0

    def test_score_capped_at_zero(self):
        r = QualityReport(issues=[
            QualityIssue(line=1, category="x", severity="HIGH", message="m")
            for _ in range(50)
        ])
        assert _calculate_quality_score(r) >= 0.0

    def test_high_complexity_deduction(self):
        r = QualityReport(avg_complexity=12.0)
        assert _calculate_quality_score(r) < 100.0

    def test_duplicate_deduction(self):
        r = QualityReport(duplicates=[
            DuplicateBlock(lines_a=(1, 5), lines_b=(10, 15))
            for _ in range(3)
        ])
        assert _calculate_quality_score(r) < 100.0


# --- Formatting ---

class TestFormatting:
    def test_format_text(self):
        code = "def f():\n    return 1"
        r = analyze_code_quality(code)
        text = format_quality_text(r)
        assert "Quality Score" in text
        assert "Functions" in text

    def test_format_json(self):
        code = "def f():\n    return 1"
        r = analyze_code_quality(code)
        j = format_quality_json(r)
        data = json.loads(j)
        assert "quality_score" in data
        assert "functions" in data

    def test_format_text_with_issues(self):
        lines = ["def long_func():"]
        for i in range(110):
            lines.append(f"    x_{i} = {i}")
        code = "\n".join(lines)
        r = analyze_code_quality(code)
        text = format_quality_text(r)
        assert "HIGH" in text

    def test_format_empty(self):
        r = analyze_code_quality("")
        text = format_quality_text(r)
        assert "EXCELLENT" in text


# --- QualityIssue ---

class TestQualityIssue:
    def test_to_dict(self):
        qi = QualityIssue(line=10, category="length", severity="HIGH",
                          message="Too long")
        d = qi.to_dict()
        assert d["line"] == 10
        assert d["severity"] == "HIGH"
