"""
Code quality and test quality analysis utilities.

Provides:
- get_dead_code: Find dead/unused code using vulture
- get_maintainability_index: Get maintainability metrics using radon
- get_slow_tests: Find slow tests using pytest --durations
- get_flaky_tests: Detect flaky tests using pytest-randomly
- get_security_issues: Find security issues using bandit
- get_test_structure: Collect test structure using pytest --collect-only
- get_test_patterns: Detect existing test patterns and fixtures
"""

import ast
import json
import sys
from pathlib import Path

from .utils import get_cache_dir, run_command


def get_test_structure(project_path: str, test_dir: str = "tests") -> dict:
    """Collect tests using pytest --collect-only."""
    cache_dir = get_cache_dir(project_path)
    cmd = [
        sys.executable,
        "-m",
        "pytest",
        test_dir,
        "--collect-only",
        "-q",
        "--ignore=node_modules",
        "--ignore=venv",
        f"-o=cache_dir={cache_dir / 'pytest_cache'}",
    ]

    _, stdout, stderr = run_command(cmd, cwd=project_path, timeout=120)

    tests = []
    test_files = set()

    for line in stdout.strip().split("\n"):
        line = line.strip()
        if "::" in line and not line.startswith(("=", "-", " ")):
            parts = line.split("::")
            if len(parts) >= 2:
                file_path = parts[0]
                test_name = parts[-1]
                test_class = parts[1] if len(parts) > 2 else None

                test_files.add(file_path)
                tests.append(
                    {
                        "file": file_path,
                        "class": test_class,
                        "name": test_name,
                        "full_path": line,
                    }
                )

    return {"test_count": len(tests), "test_files": sorted(test_files), "tests": tests}


def get_dead_code(project_path: str) -> list[dict]:
    """Find dead/unused code using vulture."""
    cmd = ["vulture", ".", "--min-confidence", "80"]
    _, stdout, _ = run_command(cmd, cwd=project_path, timeout=120)

    dead_code = []
    for line in stdout.strip().split("\n"):
        if not line.strip():
            continue
        if "unused" in line.lower():
            parts = line.split(":")
            if len(parts) >= 3:
                file_path = parts[0]
                line_num = parts[1]
                description = ":".join(parts[2:]).strip()

                if "test" in file_path.lower():
                    continue

                dead_code.append(
                    {
                        "file": file_path,
                        "line": int(line_num) if line_num.isdigit() else 0,
                        "description": description,
                    }
                )

    return dead_code


def get_maintainability_index(project_path: str) -> list[dict]:
    """Get maintainability index using radon mi."""
    cmd = ["radon", "mi", ".", "-j"]
    _, stdout, _ = run_command(cmd, cwd=project_path, timeout=120)

    results = []
    try:
        data = json.loads(stdout)
        for filepath, info in data.items():
            if "test" in filepath.lower():
                continue

            mi_score = info.get("mi", 100)
            rank = info.get("rank", "A")

            if rank in ("C", "D", "E", "F"):
                results.append(
                    {
                        "file": filepath,
                        "maintainability_index": round(mi_score, 1),
                        "rank": rank,
                        "reason": f"low maintainability ({rank}, MI={mi_score:.0f})",
                    }
                )
    except json.JSONDecodeError:
        pass

    return results


def get_slow_tests(
    project_path: str,
    test_dir: str = "tests",
    threshold: float = 1.0,
    test_health: dict | None = None,
) -> list[dict]:
    """Find slow tests using pytest --durations.

    Args:
        project_path: Path to the project
        test_dir: Test directory name
        threshold: Minimum duration in seconds to report
        test_health: Optional test health data to cross-reference failing tests

    Returns:
        List of slow tests with timing_reliable flag.
    """
    cache_dir = get_cache_dir(project_path)
    cmd = [
        sys.executable,
        "-m",
        "pytest",
        test_dir,
        "--durations=0",
        "-q",
        "--no-header",
        "--timeout=300",
        f"-o=cache_dir={cache_dir / 'pytest_cache'}",
    ]

    env = {"COVERAGE_FILE": str(cache_dir / ".coverage")}
    _, stdout, _ = run_command(cmd, cwd=project_path, timeout=600, env=env)

    failing_tests = set()
    if test_health and test_health.get("failing_tests"):
        failing_tests = {ft["test"] for ft in test_health["failing_tests"]}

    slow_tests = []
    in_durations = False

    for line in stdout.strip().split("\n"):
        line = line.strip()
        if "slowest" in line.lower() or "durations" in line.lower():
            in_durations = True
            continue

        if in_durations and line:
            parts = line.split()
            if len(parts) >= 3 and parts[0].endswith("s"):
                try:
                    duration_str = parts[0].rstrip("s")
                    duration = float(duration_str)
                    if duration >= threshold:
                        test_path = parts[-1]
                        is_failing = test_path in failing_tests

                        slow_tests.append(
                            {
                                "test": test_path,
                                "duration_seconds": duration,
                                "reason": f"slow test ({duration:.2f}s)",
                                "timing_reliable": not is_failing,
                                "note": (
                                    "timing may be due to connection timeout or error"
                                    if is_failing
                                    else None
                                ),
                            }
                        )
                except ValueError:
                    continue

    slow_tests.sort(key=lambda x: (not x["timing_reliable"], -x["duration_seconds"]))

    return slow_tests


def get_flaky_tests(
    project_path: str, test_dir: str = "tests", runs: int = 3
) -> list[dict]:
    """Detect flaky/order-dependent tests by running with random ordering.

    Uses pytest-randomly to shuffle test order multiple times.
    """
    cache_dir = get_cache_dir(project_path)
    results_per_test = {}
    env = {"COVERAGE_FILE": str(cache_dir / ".coverage")}

    for i in range(runs):
        cmd = [
            sys.executable,
            "-m",
            "pytest",
            test_dir,
            "-p",
            "randomly",
            f"--randomly-seed={i * 12345}",
            "-q",
            "--no-header",
            "--timeout=120",
            "--tb=no",
            f"-o=cache_dir={cache_dir / 'pytest_cache'}",
        ]

        _, stdout, _ = run_command(cmd, cwd=project_path, timeout=300, env=env)

        for line in stdout.strip().split("\n"):
            line = line.strip()
            if "::" in line:
                if " PASSED" in line or line.endswith(" ."):
                    test_path = line.split()[0] if " " in line else line.rstrip(" .")
                    if test_path not in results_per_test:
                        results_per_test[test_path] = []
                    results_per_test[test_path].append("pass")
                elif " FAILED" in line or " ERROR" in line:
                    test_path = line.split()[0]
                    if test_path not in results_per_test:
                        results_per_test[test_path] = []
                    results_per_test[test_path].append("fail")

    flaky_tests = []
    for test_path, results in results_per_test.items():
        if len(set(results)) > 1:
            pass_count = results.count("pass")
            fail_count = results.count("fail")
            flaky_tests.append(
                {
                    "test": test_path,
                    "passes": pass_count,
                    "failures": fail_count,
                    "reason": f"flaky test - passed {pass_count}/{runs}, failed {fail_count}/{runs}",
                    "priority": "high",
                }
            )

    return flaky_tests


def get_security_issues(project_path: str) -> list[dict]:
    """Find security issues using bandit."""
    cmd = ["bandit", "-r", ".", "-f", "json", "-q"]
    _, stdout, _ = run_command(cmd, cwd=project_path, timeout=120)

    issues = []
    try:
        data = json.loads(stdout)
        for result in data.get("results", []):
            if "test" in result.get("filename", "").lower():
                continue

            severity = result.get("issue_severity", "LOW")
            confidence = result.get("issue_confidence", "LOW")

            if severity in ("MEDIUM", "HIGH") and confidence in ("MEDIUM", "HIGH"):
                issues.append(
                    {
                        "file": result.get("filename", ""),
                        "line": result.get("line_number", 0),
                        "issue": result.get("issue_text", ""),
                        "severity": severity,
                        "confidence": confidence,
                        "test_id": result.get("test_id", ""),
                        "reason": f"security issue: {result.get('issue_text', '')[:50]}",
                        "priority": "high" if severity == "HIGH" else "medium",
                    }
                )
    except json.JSONDecodeError:
        pass

    return issues


def get_test_patterns(project_path: str, test_dir: str = "tests") -> dict:
    """Detect existing test patterns, fixtures, and infrastructure.

    Helps understand what testing utilities are available.
    """
    test_path = Path(project_path) / test_dir
    if not test_path.exists():
        return {"error": f"Test directory not found: {test_dir}"}

    patterns = {
        "fixtures": [],
        "parametrize": [],
        "mocks": [],
        "markers": set(),
        "conftest_files": [],
        "factory_patterns": [],
        "helper_functions": [],
    }

    for conftest in test_path.rglob("conftest.py"):
        rel_path = str(conftest.relative_to(project_path))
        patterns["conftest_files"].append(rel_path)

        try:
            content = conftest.read_text()
            tree = ast.parse(content)

            for node in ast.walk(tree):
                if isinstance(node, ast.FunctionDef):
                    for decorator in node.decorator_list:
                        dec_name = ""
                        if isinstance(decorator, ast.Name):
                            dec_name = decorator.id
                        elif isinstance(decorator, ast.Attribute):
                            dec_name = decorator.attr
                        elif isinstance(decorator, ast.Call):
                            if isinstance(decorator.func, ast.Name):
                                dec_name = decorator.func.id
                            elif isinstance(decorator.func, ast.Attribute):
                                dec_name = decorator.func.attr

                        if dec_name == "fixture":
                            patterns["fixtures"].append(
                                {
                                    "name": node.name,
                                    "file": rel_path,
                                    "line": node.lineno,
                                }
                            )
        except (SyntaxError, IOError):
            continue

    for test_file in test_path.rglob("test_*.py"):
        rel_path = str(test_file.relative_to(project_path))

        try:
            content = test_file.read_text()
            tree = ast.parse(content)

            for node in ast.walk(tree):
                if isinstance(node, ast.FunctionDef):
                    for decorator in node.decorator_list:
                        dec_name = ""
                        if isinstance(decorator, ast.Name):
                            dec_name = decorator.id
                        elif isinstance(decorator, ast.Attribute):
                            dec_name = decorator.attr
                        elif isinstance(decorator, ast.Call):
                            if isinstance(decorator.func, ast.Name):
                                dec_name = decorator.func.id
                            elif isinstance(decorator.func, ast.Attribute):
                                dec_name = decorator.func.attr

                        if dec_name == "parametrize":
                            patterns["parametrize"].append(
                                {
                                    "test": node.name,
                                    "file": rel_path,
                                    "line": node.lineno,
                                }
                            )

                        if dec_name == "mark":
                            if isinstance(decorator, ast.Call) and isinstance(
                                decorator.func, ast.Attribute
                            ):
                                if hasattr(decorator.func, "attr"):
                                    patterns["markers"].add(decorator.func.attr)

            if "mock" in content.lower() or "patch" in content.lower():
                for node in ast.walk(tree):
                    if isinstance(node, ast.Call):
                        func_name = ""
                        if isinstance(node.func, ast.Attribute):
                            func_name = node.func.attr
                        elif isinstance(node.func, ast.Name):
                            func_name = node.func.id

                        if func_name == "patch" and node.args:
                            if isinstance(node.args[0], ast.Constant):
                                patterns["mocks"].append(
                                    {
                                        "target": node.args[0].value,
                                        "file": rel_path,
                                    }
                                )

            for node in ast.walk(tree):
                if isinstance(node, ast.FunctionDef):
                    name = node.name.lower()
                    if name.startswith(
                        ("create_", "make_", "build_", "factory_")
                    ) or name.endswith("_factory"):
                        patterns["factory_patterns"].append(
                            {
                                "name": node.name,
                                "file": rel_path,
                                "line": node.lineno,
                            }
                        )
                    elif not name.startswith("test_") and not name.startswith("_"):
                        patterns["helper_functions"].append(
                            {
                                "name": node.name,
                                "file": rel_path,
                                "line": node.lineno,
                            }
                        )

        except (SyntaxError, IOError):
            continue

    patterns["markers"] = sorted(patterns["markers"])

    seen_mocks = set()
    unique_mocks = []
    for mock in patterns["mocks"]:
        if mock["target"] not in seen_mocks:
            seen_mocks.add(mock["target"])
            unique_mocks.append(mock)
    patterns["mocks"] = unique_mocks

    patterns["summary"] = {
        "fixture_count": len(patterns["fixtures"]),
        "parametrize_count": len(patterns["parametrize"]),
        "mock_targets": len(patterns["mocks"]),
        "markers": patterns["markers"],
        "conftest_count": len(patterns["conftest_files"]),
        "factory_count": len(patterns["factory_patterns"]),
    }

    return patterns
