import re
import shutil
import subprocess
import tempfile
import zipfile
from pathlib import Path


def run_command(cmd: list[str], cwd: Path | None = None) -> tuple[int, str, str]:
    result = subprocess.run(
        cmd,
        cwd=cwd,
        capture_output=True,
        text=True,
    )
    return result.returncode, result.stdout, result.stderr


def build_wheel(project_dir: Path) -> Path:
    with tempfile.TemporaryDirectory() as tmpdir:
        exit_code, stdout, stderr = run_command(
            ["uv", "build", "--wheel", "-o", tmpdir],
            cwd=project_dir,
        )
        if exit_code != 0:
            raise RuntimeError(f"Build failed: {stderr}")

        wheels = list(Path(tmpdir).glob("*.whl"))
        if len(wheels) != 1:
            raise ValueError(f"Expected 1 wheel, found {len(wheels)}")

        # Copy wheel to a persistent location
        persistent_tmpdir = Path(tempfile.mkdtemp())
        wheel_dest = persistent_tmpdir / wheels[0].name
        shutil.copy(wheels[0], wheel_dest)
        return wheel_dest


def extract_metadata_from_wheel(wheel_path: Path) -> str:
    with zipfile.ZipFile(wheel_path, "r") as whl:
        metadata_files = [f for f in whl.namelist() if f.endswith("/METADATA")]
        if not metadata_files:
            raise ValueError(f"No METADATA file found in {wheel_path}")
        return whl.read(metadata_files[0]).decode("utf-8")


def get_requires_dist_lines(metadata: str) -> list[str]:
    return [line for line in metadata.split("\n") if line.startswith("Requires-Dist:")]


def extract_package_names(requires_dist_lines: list[str]) -> list[str]:
    package_names = []
    for dep in requires_dist_lines:
        parts = dep.split(":", 1)[1].strip()
        pkg_name = re.split(r"[<>=!]", parts)[0].strip()
        package_names.append(pkg_name)
    return package_names


def assert_no_local_paths(dependency_line: str) -> None:
    assert "file://" not in dependency_line, f"Contains local path: {dependency_line}"
    assert "path=" not in dependency_line, f"Contains path-based dependency: {dependency_line}"


def assert_version_constraint(dependency_line: str, min_version: str, max_version: str) -> None:
    pattern1 = rf">={re.escape(min_version)},<{re.escape(max_version)}"
    pattern2 = rf"<{re.escape(max_version)},>={re.escape(min_version)}"
    assert re.search(pattern1, dependency_line) or re.search(pattern2, dependency_line), (
        f"Missing or incorrect version constraint (expected >={min_version},<{max_version}): {dependency_line}"
    )


class TestPluginDependencies:
    def test_mistralai_plugin_wheel_has_pypi_dependencies(self):
        plugin_dir = Path(__file__).parent.parent / "plugins" / "mistralai"
        wheel_path = build_wheel(plugin_dir)

        try:
            metadata = extract_metadata_from_wheel(wheel_path)
            requires_dist = get_requires_dist_lines(metadata)

            # Should have mistralai-workflows with version constraint
            workflows_deps = [dep for dep in requires_dist if "mistralai-workflows" in dep]
            assert len(workflows_deps) == 1, f"Expected 1 mistralai-workflows dep, found {len(workflows_deps)}"

            # Should NOT contain file:// or local path
            assert_no_local_paths(workflows_deps[0])

            # Should have version constraint
            assert_version_constraint(workflows_deps[0], "2.0.0", "2.1.0")

            # Verify other expected dependencies
            dep_names = extract_package_names(requires_dist)
            assert "mistralai" in dep_names
            assert "structlog" in dep_names
        finally:
            shutil.rmtree(wheel_path.parent, ignore_errors=True)

    def test_agents_plugin_wheel_has_pypi_dependencies(self):
        plugin_dir = Path(__file__).parent.parent / "plugins" / "agents"
        wheel_path = build_wheel(plugin_dir)

        try:
            metadata = extract_metadata_from_wheel(wheel_path)
            requires_dist = get_requires_dist_lines(metadata)

            # Should have both plugin dependencies
            workflows_deps = [dep for dep in requires_dist if "mistralai-workflows" in dep and "plugins" not in dep]
            mistralai_plugin_deps = [dep for dep in requires_dist if "mistralai-workflows-plugins-mistralai" in dep]

            assert len(workflows_deps) == 1, "Expected 1 mistralai-workflows dep"
            assert len(mistralai_plugin_deps) == 1, "Expected 1 mistralai-workflows-plugins-mistralai dep"

            # Should NOT contain local paths
            for dep in [workflows_deps[0], mistralai_plugin_deps[0]]:
                assert_no_local_paths(dep)

            # Should have version constraints
            for dep in [workflows_deps[0], mistralai_plugin_deps[0]]:
                assert_version_constraint(dep, "2.0.0", "2.1.0")
        finally:
            shutil.rmtree(wheel_path.parent, ignore_errors=True)

    def test_main_sdk_wheel_has_plugin_extras(self):
        sdk_dir = Path(__file__).parent.parent
        wheel_path = build_wheel(sdk_dir)

        try:
            metadata = extract_metadata_from_wheel(wheel_path)

            # Check for extras
            extras = [line for line in metadata.split("\n") if line.startswith("Provides-Extra:")]
            extra_names = [line.split(":")[1].strip() for line in extras]

            assert "mistralai" in extra_names, "Missing 'mistralai' extra"
            assert "agents" in extra_names, "Missing 'agents' extra"
            assert "all-plugins" in extra_names, "Missing 'all-plugins' extra"

            # Check that extra dependencies use PyPI, not local paths
            extra_deps = [
                line for line in metadata.split("\n") if line.startswith("Requires-Dist:") and "extra ==" in line
            ]

            for dep in extra_deps:
                assert_no_local_paths(dep)
        finally:
            shutil.rmtree(wheel_path.parent, ignore_errors=True)

    def test_uv_sources_not_in_wheel_metadata(self):
        plugin_dir = Path(__file__).parent.parent / "plugins" / "mistralai"
        wheel_path = build_wheel(plugin_dir)

        try:
            with zipfile.ZipFile(wheel_path, "r") as whl:
                # Check that no file contains [tool.uv.sources]
                for filename in whl.namelist():
                    content = whl.read(filename).decode("utf-8", errors="ignore")
                    assert "[tool.uv.sources]" not in content, (
                        f"Found [tool.uv.sources] in {filename} - should be stripped during build"
                    )
        finally:
            shutil.rmtree(wheel_path.parent, ignore_errors=True)

    def test_local_dev_uses_path_sources(self):
        sdk_dir = Path(__file__).parent.parent

        pyproject = (sdk_dir / "pyproject.toml").read_text()
        assert "[tool.uv.sources]" in pyproject, "Missing [tool.uv.sources] for local dev"
        assert 'path = "plugins/mistralai"' in pyproject or 'path = "plugins/agents"' in pyproject, (
            "Missing plugin path sources"
        )

    def test_plugins_declare_sdk_dependency(self):
        plugins = ["mistralai", "agents"]
        sdk_dir = Path(__file__).parent.parent

        for plugin_name in plugins:
            pyproject_path = sdk_dir / "plugins" / plugin_name / "pyproject.toml"
            pyproject = pyproject_path.read_text()

            # Should have mistralai-workflows in dependencies
            assert "mistralai-workflows>=2.0.0,<2.1.0" in pyproject, (
                f"{plugin_name} plugin missing mistralai-workflows dependency"
            )

            # Should have [tool.uv.sources] for local dev
            assert "[tool.uv.sources]" in pyproject, f"{plugin_name} plugin missing [tool.uv.sources] for local dev"
            assert 'mistralai-workflows = { path = "../.."' in pyproject, (
                f"{plugin_name} plugin missing path source for SDK"
            )
