"""Tests for AST-based code duplication detection (Type 1 & Type 2)."""

from vibelexity.analyzers.python import analyze_source as analyze_py
from vibelexity.analyzers.typescript import analyze_source as analyze_ts
from vibelexity.analyzers.go import analyze_source as analyze_go
from vibelexity.metrics.duplication.core import find_duplicates


# ── Python helpers ────────────────────────────────────────────────────


def _py_hashes(code: str):
    """Return (type1_hash, type2_hash) of the first function in Python code."""
    result = analyze_py(code)
    assert result.functions, f"No functions found in:\n{code}"
    f = result.functions[0]
    return f.type1_hash, f.type2_hash


# ── Type 1: Exact clones ─────────────────────────────────────────────


def test_py_identical_functions_same_type1():
    code = """
def foo(x, y):
    if x > y:
        return x
    return y
"""
    h1a, _ = _py_hashes(code)
    h1b, _ = _py_hashes(code)
    assert h1a is not None
    assert h1a == h1b


def test_py_comments_ignored_type1():
    code_a = """
def foo(x, y):
    if x > y:
        return x
    return y
"""
    code_b = """
def foo(x, y):
    # this is a comment
    if x > y:
        return x  # inline comment
    return y
"""
    h1a, _ = _py_hashes(code_a)
    h1b, _ = _py_hashes(code_b)
    assert h1a is not None
    assert h1a == h1b


def test_py_different_names_different_type1():
    code_a = """
def foo(x, y):
    if x > y:
        return x
    return y
"""
    code_b = """
def bar(a, b):
    if a > b:
        return a
    return b
"""
    h1a, _ = _py_hashes(code_a)
    h1b, _ = _py_hashes(code_b)
    assert h1a is not None and h1b is not None
    assert h1a != h1b


def test_py_docstrings_ignored_type1():
    code_a = """
def foo(x, y):
    if x > y:
        return x
    return y
"""
    code_b = """
def foo(x, y):
    \"\"\"Return the larger value.\"\"\"
    if x > y:
        return x
    return y
"""
    h1a, _ = _py_hashes(code_a)
    h1b, _ = _py_hashes(code_b)
    assert h1a is not None
    assert h1a == h1b


def test_py_docstrings_ignored_type2():
    code_a = """
def foo(x, y):
    if x > y:
        return x
    return y
"""
    code_b = """
def bar(a, b):
    \"\"\"Return the larger value.\"\"\"
    if a > b:
        return a
    return b
"""
    _, h2a = _py_hashes(code_a)
    _, h2b = _py_hashes(code_b)
    assert h2a is not None
    assert h2a == h2b


def test_py_comments_ignored_type2():
    code_a = """
def foo(x, y):
    if x > y:
        return x
    return y
"""
    code_b = """
def foo(x, y):
    # this is a comment
    if x > y:
        return x  # inline comment
    return y
"""
    _, h2a = _py_hashes(code_a)
    _, h2b = _py_hashes(code_b)
    assert h2a is not None
    assert h2a == h2b


# ── Type 2: Parametric clones ────────────────────────────────────────


def test_py_renamed_identifiers_same_type2():
    code_a = """
def foo(x, y):
    if x > y:
        return x
    return y
"""
    code_b = """
def bar(a, b):
    if a > b:
        return a
    return b
"""
    _, h2a = _py_hashes(code_a)
    _, h2b = _py_hashes(code_b)
    assert h2a is not None and h2b is not None
    assert h2a == h2b


def test_py_different_literals_same_type2():
    code_a = """
def foo(x):
    if x > 10:
        return 20
    return 30
"""
    code_b = """
def foo(x):
    if x > 99:
        return 200
    return 300
"""
    _, h2a = _py_hashes(code_a)
    _, h2b = _py_hashes(code_b)
    assert h2a is not None and h2b is not None
    assert h2a == h2b


def test_py_structurally_different_functions():
    code_a = """
def foo(x, y):
    if x > y:
        return x
    return y
"""
    code_b = """
def foo(x, y, z):
    while x > y:
        x = x - z
    return x
"""
    h1a, h2a = _py_hashes(code_a)
    h1b, h2b = _py_hashes(code_b)
    assert h1a != h1b
    assert h2a != h2b


# ── Trivial functions ────────────────────────────────────────────────


def test_py_trivial_function_returns_none():
    code = """
def foo():
    pass
"""
    h1, h2 = _py_hashes(code)
    assert h1 is None
    assert h2 is None


# ── TypeScript ────────────────────────────────────────────────────────


def _ts_hashes(code: str):
    """Return (type1_hash, type2_hash) of the first function in TypeScript code."""
    result = analyze_ts(code)
    assert result.functions, f"No functions found in:\n{code}"
    f = result.functions[0]
    return f.type1_hash, f.type2_hash


def test_ts_identical_functions_same_type1():
    code = """
function max(x: number, y: number): number {
    if (x > y) {
        return x;
    }
    return y;
}
"""
    h1a, _ = _ts_hashes(code)
    h1b, _ = _ts_hashes(code)
    assert h1a is not None
    assert h1a == h1b


def test_ts_renamed_identifiers_same_type2():
    code_a = """
function max(x: number, y: number): number {
    if (x > y) {
        return x;
    }
    return y;
}
"""
    code_b = """
function biggest(a: number, b: number): number {
    if (a > b) {
        return a;
    }
    return b;
}
"""
    h1a, h2a = _ts_hashes(code_a)
    h1b, h2b = _ts_hashes(code_b)
    assert h1a != h1b
    assert h2a is not None and h2b is not None
    assert h2a == h2b


def test_ts_different_literals_same_type2():
    code_a = """
function check(x: number): number {
    if (x > 10) {
        return 20;
    }
    return 30;
}
"""
    code_b = """
function check(x: number): number {
    if (x > 99) {
        return 200;
    }
    return 300;
}
"""
    _, h2a = _ts_hashes(code_a)
    _, h2b = _ts_hashes(code_b)
    assert h2a is not None and h2b is not None
    assert h2a == h2b


def test_ts_trivial_function_returns_none():
    code = """
function noop() {}
"""
    h1, h2 = _ts_hashes(code)
    assert h1 is None
    assert h2 is None


# ── Go ────────────────────────────────────────────────────────────────


def _go_hashes(code: str):
    """Return (type1_hash, type2_hash) of the first function in Go code."""
    result = analyze_go(code)
    assert result.functions, f"No functions found in:\n{code}"
    f = result.functions[0]
    return f.type1_hash, f.type2_hash


def test_go_identical_functions_same_type1():
    code = """
package main

func max(x int, y int) int {
    if x > y {
        return x
    }
    return y
}
"""
    h1a, _ = _go_hashes(code)
    h1b, _ = _go_hashes(code)
    assert h1a is not None
    assert h1a == h1b


def test_go_renamed_identifiers_same_type2():
    code_a = """
package main

func max(x int, y int) int {
    if x > y {
        return x
    }
    return y
}
"""
    code_b = """
package main

func biggest(a int, b int) int {
    if a > b {
        return a
    }
    return b
}
"""
    h1a, h2a = _go_hashes(code_a)
    h1b, h2b = _go_hashes(code_b)
    assert h1a != h1b
    assert h2a is not None and h2b is not None
    assert h2a == h2b


def test_go_different_literals_same_type2():
    code_a = """
package main

func check(x int) int {
    if x > 10 {
        return 20
    }
    return 30
}
"""
    code_b = """
package main

func check(x int) int {
    if x > 99 {
        return 200
    }
    return 300
}
"""
    _, h2a = _go_hashes(code_a)
    _, h2b = _go_hashes(code_b)
    assert h2a is not None and h2b is not None
    assert h2a == h2b


def test_go_trivial_function_returns_none():
    code = """
package main

func noop() {}
"""
    h1, h2 = _go_hashes(code)
    assert h1 is None
    assert h2 is None


# ── find_duplicates integration ──────────────────────────────────────


def test_find_duplicates_groups_type1():
    code = """
def foo(x, y):
    if x > y:
        return x
    return y

def bar(x, y):
    if x > y:
        return x
    return y
"""
    result = analyze_py(code)
    result.path = "test.py"
    duplicates = find_duplicates([result])
    type1 = [c for c in duplicates if c.dup_type == 1]
    assert len(type1) == 1
    assert len(type1[0].members) == 2


def test_find_duplicates_groups_type2_only():
    code = """
def foo(x, y):
    if x > y:
        return x
    return y

def bar(a, b):
    if a > b:
        return a
    return b
"""
    result = analyze_py(code)
    result.path = "test.py"
    duplicates = find_duplicates([result])
    type1 = [c for c in duplicates if c.dup_type == 1]
    type2 = [c for c in duplicates if c.dup_type == 2]
    assert len(type1) == 0
    assert len(type2) == 1
    assert len(type2[0].members) == 2


def test_find_duplicates_type1_suppresses_type2():
    """When two functions are exact clones (Type 1), they shouldn't also appear as Type 2."""
    code = """
def foo(x, y):
    if x > y:
        return x
    return y

def bar(x, y):
    if x > y:
        return x
    return y
"""
    result = analyze_py(code)
    result.path = "test.py"
    duplicates = find_duplicates([result])
    type1 = [c for c in duplicates if c.dup_type == 1]
    type2 = [c for c in duplicates if c.dup_type == 2]
    assert len(type1) == 1
    assert len(type2) == 0


def test_find_duplicates_no_clusters_for_unique():
    code = """
def foo(x, y):
    if x > y:
        return x
    return y

def bar(x, y, z):
    while x > y:
        x = x - z
    return x
"""
    result = analyze_py(code)
    result.path = "test.py"
    duplicates = find_duplicates([result])
    assert len(duplicates) == 0


def test_find_duplicates_type2_excludes_1_liners():
    """Type 2 duplications should exclude 1-liners from counting as duplicates."""
    code = """
def foo(x):
    return x

def bar(x):
    return x
"""
    result = analyze_py(code)
    result.path = "test.py"
    duplicates = find_duplicates([result])
    type2 = [c for c in duplicates if c.dup_type == 2]
    assert len(type2) == 0


def test_find_duplicates_type2_detects_2_liners():
    """Type 2 duplications should still detect functions with 2+ lines as duplicates."""
    code = """
def foo(x, y):
    if x > y:
        return x
    return y

def bar(a, b):
    if a > b:
        return a
    return b
"""
    result = analyze_py(code)
    result.path = "test.py"
    duplicates = find_duplicates([result])
    type2 = [c for c in duplicates if c.dup_type == 2]
    assert len(type2) == 1
    assert len(type2[0].members) == 2


def test_find_duplicates_type2_excludes_multi_line_single_statement():
    """Multi-line split of a single statement should still be filtered as 1-liner."""
    code = """
def foo(x, y):
    return (
        x + y + x + y + 1
    )

def bar(a, b):
    return (
        a + b + a + b + 1
    )
"""
    result = analyze_py(code)
    result.path = "test.py"
    duplicates = find_duplicates([result])
    type2 = [c for c in duplicates if c.dup_type == 2]
    assert len(type2) == 0


def test_go_statement_counting():
    """Type 2 detection should work correctly for Go with statement_list."""
    code = """
package main

func max(x int, y int) int {
    if x > y {
        return x
    }
    return y
}

func min(a int, b int) int {
    if a < b {
        return a
    }
    return b
}
"""
    result = analyze_go(code)
    result.path = "test.go"
    duplicates = find_duplicates([result])
    type2 = [c for c in duplicates if c.dup_type == 2]
    assert len(type2) == 1
    assert len(type2[0].members) == 2


def test_ts_statement_counting():
    """Type 2 detection should work correctly for TypeScript."""
    code = """
function max(x: number, y: number): number {
    if (x > y) {
        return x;
    }
    return y;
}

function min(a: number, b: number): number {
    if (a < b) {
        return a;
    }
    return b;
}
"""
    result = analyze_ts(code)
    result.path = "test.ts"
    duplicates = find_duplicates([result])
    type2 = [c for c in duplicates if c.dup_type == 2]
    assert len(type2) == 1
    assert len(type2[0].members) == 2


def test_find_duplicates_cross_file():
    code_a = """
def foo(x, y):
    if x > y:
        return x
    return y
"""
    code_b = """
def bar(a, b):
    if a > b:
        return a
    return b
"""
    result_a = analyze_py(code_a)
    result_a.path = "file_a.py"
    result_b = analyze_py(code_b)
    result_b.path = "file_b.py"
    duplicates = find_duplicates([result_a, result_b])
    type2 = [c for c in duplicates if c.dup_type == 2]
    assert len(type2) == 1
    files = {fpath for fpath, _ in type2[0].members}
    assert files == {"file_a.py", "file_b.py"}
