import pytest
from pathlib import Path
import importlib.metadata
import tomllib  # Standard in Python 3.11+

def get_root_path():
    return Path(__file__).parent.parent

def _clean_string(req):
    """Normalizes a requirement string (lowercase and removes spaces)."""
    return req.strip().lower().replace(" ", "")

def parse_txt_requirements(file_path):
    """Extracts full requirement strings (name + version) from a .txt file."""
    if not file_path.exists():
        return set()
    packages = set()
    with open(file_path, "r") as f:
        for line in f:
            line = line.strip()
            # Ignore empty lines, comments, and recursive/local flags
            if not line or line.startswith(("#", "-e", "git+", "-r")):
                continue
            packages.add(_clean_string(line))
    return packages

def parse_toml_section(data, section_type="main"):
    """Extracts full requirement strings from pyproject.toml."""
    packages = set()
    if section_type == "main":
        deps = data.get("project", {}).get("dependencies", [])
    else:
        # Check optional-dependencies or dependency-groups
        deps = data.get("project", {}).get("optional-dependencies", {}).get("dev", [])
        if not deps:
            deps = data.get("dependency-groups", {}).get("dev", [])
            
    for req in deps:
        packages.add(_clean_string(req))
    return packages

def test_dependency_files_integrity():
    """1. Verifies that .txt files and pyproject.toml have identical names AND versions."""
    root = get_root_path()
    toml_path = root / "pyproject.toml"
    assert toml_path.exists(), "Missing pyproject.toml"

    with open(toml_path, "rb") as f:
        toml_data = tomllib.load(f)

    # Validate Production Sync
    txt_main = parse_txt_requirements(root / "requirements.txt")
    toml_main = parse_toml_section(toml_data, "main")
    assert txt_main == toml_main, (
        f"Production version mismatch!\n"
        f"Only in TXT: {txt_main - toml_main}\n"
        f"Only in TOML: {toml_main - txt_main}"
    )

    # Validate Development Sync
    txt_dev = parse_txt_requirements(root / "requirements-dev.txt")
    toml_dev = parse_toml_section(toml_data, "dev")
    assert txt_dev == toml_dev, (
        f"Development version mismatch!\n"
        f"Only in TXT: {txt_dev - toml_dev}\n"
        f"Only in TOML: {toml_dev - txt_dev}"
    )

def test_environment_vs_toml():
    """2. Verifies that installed packages satisfy TOML requirements."""
    root = get_root_path()
    with open(root / "pyproject.toml", "rb") as f:
        data = tomllib.load(f)
    
    all_declared = parse_toml_section(data, "main") | parse_toml_section(data, "dev")
    _verify_installation(all_declared, "TOML")

def test_environment_vs_requirements():
    """3. Verifies that installed packages satisfy .txt requirements."""
    root = get_root_path()
    all_txt_deps = (
        parse_txt_requirements(root / "requirements.txt") | 
        parse_txt_requirements(root / "requirements-dev.txt")
    )
    _verify_installation(all_txt_deps, "requirements.txt")

def _verify_installation(package_set, source_name):
    """Helper to check if declared versions match installed versions."""
    missing_or_wrong = []
    
    for req in package_set:
        # Split name from version to check installation status
        # handles ==, >=, ~=, <=, > , <
        import re
        parts = re.split(r'==|>=|~=|<=|>|<', req)
        name = parts[0].strip()
        
        try:
            installed_ver = importlib.metadata.version(name)
            # If the config uses exact versioning '==', we can do a strict check
            if "==" in req:
                expected_ver = req.split("==")[1].strip()
                if installed_ver != expected_ver:
                    missing_or_wrong.append(f"{name} (Installed: {installed_ver}, Expected: {expected_ver})")
        except importlib.metadata.PackageNotFoundError:
            missing_or_wrong.append(f"{name} (Not installed)")

    if missing_or_wrong:
        pytest.fail(f"Environment out of sync with {source_name}:\n" + "\n".join(missing_or_wrong))