"""Tests for ``mld link`` and ``mld unlink`` commands."""

from __future__ import annotations

import argparse
import subprocess
from pathlib import Path
from unittest.mock import patch

import pytest

from mld_sdk.link_command import (
    _find_platform_root,
    _find_sibling_plugins,
    _link_plugin_frontend,
    _link_plugin_python,
    _register_js_sdk,
    _unlink_plugin_frontend,
    _unlink_plugin_python,
    cmd_link,
    cmd_unlink,
)


@pytest.fixture
def workspace(tmp_path: Path):
    """Create a fake workspace with platform + plugins."""
    # Platform
    platform = tmp_path / "mld"
    (platform / "packages" / "sdk-python").mkdir(parents=True)
    (platform / "packages" / "sdk-python" / "pyproject.toml").write_text(
        "[project]\nname='mld-sdk'\n"
    )
    (platform / "packages" / "sdk-frontend").mkdir(parents=True)
    (platform / "packages" / "sdk-frontend" / "package.json").write_text(
        '{"name":"@morscherlab/mld-sdk"}'
    )

    # Plugin with both python and frontend
    plugin1 = tmp_path / "mld-plugin-drp"
    plugin1.mkdir()
    (plugin1 / "pyproject.toml").write_text("[project]\nname='mld-plugin-drp'\n")
    (plugin1 / ".venv").mkdir()
    (plugin1 / "frontend").mkdir()
    (plugin1 / "frontend" / "package.json").write_text('{"name":"drp-frontend"}')

    # Plugin with only python (no frontend, no .venv)
    plugin2 = tmp_path / "mld-plugin-other"
    plugin2.mkdir()
    (plugin2 / "pyproject.toml").write_text("[project]\nname='mld-plugin-other'\n")

    return tmp_path, platform


def _ok_result(**kwargs):
    """Return a successful subprocess.CompletedProcess."""
    return subprocess.CompletedProcess(
        args=kwargs.get("args", []),
        returncode=0,
        stdout="",
        stderr="",
    )


def _fail_result(**kwargs):
    """Return a failed subprocess.CompletedProcess."""
    return subprocess.CompletedProcess(
        args=kwargs.get("args", []),
        returncode=1,
        stdout="",
        stderr="something went wrong",
    )


# ---------------------------------------------------------------------------
# _find_platform_root
# ---------------------------------------------------------------------------


class TestFindPlatformRoot:
    def test_finds_root_from_platform_dir(self, workspace):
        _, platform = workspace
        assert _find_platform_root(platform) == platform.resolve()

    def test_finds_root_from_subdirectory(self, workspace):
        _, platform = workspace
        subdir = platform / "packages" / "sdk-python"
        assert _find_platform_root(subdir) == platform.resolve()

    def test_finds_root_from_deep_subdirectory(self, workspace):
        _, platform = workspace
        deep = platform / "api" / "services"
        deep.mkdir(parents=True)
        assert _find_platform_root(deep) == platform.resolve()

    def test_exits_when_not_in_platform(self, tmp_path):
        with pytest.raises(SystemExit):
            _find_platform_root(tmp_path)


# ---------------------------------------------------------------------------
# _find_sibling_plugins
# ---------------------------------------------------------------------------


class TestFindSiblingPlugins:
    def test_finds_plugins_and_excludes_platform(self, workspace):
        _, platform = workspace
        plugins = _find_sibling_plugins(platform)
        names = [p.name for p in plugins]
        assert "mld-plugin-drp" in names
        assert "mld-plugin-other" in names
        assert "mld" not in names

    def test_excludes_dirs_without_pyproject(self, workspace):
        root, platform = workspace
        (root / "mld-no-pyproject").mkdir()
        plugins = _find_sibling_plugins(platform)
        names = [p.name for p in plugins]
        assert "mld-no-pyproject" not in names

    def test_sorted_order(self, workspace):
        _, platform = workspace
        plugins = _find_sibling_plugins(platform)
        names = [p.name for p in plugins]
        assert names == sorted(names)

    def test_empty_when_no_siblings(self, tmp_path):
        platform = tmp_path / "mld"
        platform.mkdir()
        assert _find_sibling_plugins(platform) == []


# ---------------------------------------------------------------------------
# _register_js_sdk
# ---------------------------------------------------------------------------


class TestRegisterJsSdk:
    @patch("mld_sdk.link_command.subprocess.run", return_value=_ok_result())
    def test_runs_bun_link(self, mock_run, workspace):
        _, platform = workspace
        sdk_frontend = platform / "packages" / "sdk-frontend"
        assert _register_js_sdk(sdk_frontend) is True
        mock_run.assert_called_once_with(
            ["bun", "link"],
            cwd=sdk_frontend,
            capture_output=True,
            text=True,
        )

    def test_skips_when_no_package_json(self, tmp_path):
        assert _register_js_sdk(tmp_path) is False

    @patch("mld_sdk.link_command.subprocess.run", return_value=_fail_result())
    def test_returns_false_on_failure(self, mock_run, workspace):
        _, platform = workspace
        sdk_frontend = platform / "packages" / "sdk-frontend"
        assert _register_js_sdk(sdk_frontend) is False


# ---------------------------------------------------------------------------
# _link_plugin_python / _link_plugin_frontend
# ---------------------------------------------------------------------------


class TestLinkPluginPython:
    @patch("mld_sdk.link_command.subprocess.run", return_value=_ok_result())
    def test_installs_editable(self, mock_run, workspace):
        root, platform = workspace
        plugin = root / "mld-plugin-drp"
        sdk_py = platform / "packages" / "sdk-python"
        assert _link_plugin_python(plugin, sdk_py) is True
        mock_run.assert_called_once_with(
            ["uv", "pip", "install", "-e", str(sdk_py)],
            cwd=plugin,
            capture_output=True,
            text=True,
        )

    def test_skips_when_no_venv(self, workspace):
        root, _ = workspace
        plugin = root / "mld-plugin-other"  # has no .venv
        assert _link_plugin_python(plugin, Path("/fake")) is False

    @patch("mld_sdk.link_command.subprocess.run", return_value=_fail_result())
    def test_returns_false_on_failure(self, mock_run, workspace):
        root, platform = workspace
        plugin = root / "mld-plugin-drp"
        sdk_py = platform / "packages" / "sdk-python"
        assert _link_plugin_python(plugin, sdk_py) is False


class TestLinkPluginFrontend:
    @patch("mld_sdk.link_command.subprocess.run", return_value=_ok_result())
    def test_links_frontend(self, mock_run, workspace):
        root, _ = workspace
        plugin = root / "mld-plugin-drp"
        assert _link_plugin_frontend(plugin) is True
        mock_run.assert_called_once_with(
            ["bun", "link", "@morscherlab/mld-sdk"],
            cwd=plugin / "frontend",
            capture_output=True,
            text=True,
        )

    def test_skips_when_no_frontend(self, workspace):
        root, _ = workspace
        plugin = root / "mld-plugin-other"  # no frontend dir
        assert _link_plugin_frontend(plugin) is False

    @patch("mld_sdk.link_command.subprocess.run", return_value=_fail_result())
    def test_returns_false_on_failure(self, mock_run, workspace):
        root, _ = workspace
        plugin = root / "mld-plugin-drp"
        assert _link_plugin_frontend(plugin) is False


# ---------------------------------------------------------------------------
# _unlink_plugin_python / _unlink_plugin_frontend
# ---------------------------------------------------------------------------


class TestUnlinkPluginPython:
    @patch("mld_sdk.link_command.subprocess.run", return_value=_ok_result())
    def test_runs_uv_sync(self, mock_run, workspace):
        root, _ = workspace
        plugin = root / "mld-plugin-drp"
        assert _unlink_plugin_python(plugin) is True
        mock_run.assert_called_once_with(
            ["uv", "sync"],
            cwd=plugin,
            capture_output=True,
            text=True,
        )

    def test_skips_when_no_venv(self, workspace):
        root, _ = workspace
        plugin = root / "mld-plugin-other"
        assert _unlink_plugin_python(plugin) is False


class TestUnlinkPluginFrontend:
    @patch("mld_sdk.link_command.subprocess.run", return_value=_ok_result())
    def test_runs_bun_install(self, mock_run, workspace):
        root, _ = workspace
        plugin = root / "mld-plugin-drp"
        assert _unlink_plugin_frontend(plugin) is True
        mock_run.assert_called_once_with(
            ["bun", "install"],
            cwd=plugin / "frontend",
            capture_output=True,
            text=True,
        )

    def test_skips_when_no_frontend(self, workspace):
        root, _ = workspace
        plugin = root / "mld-plugin-other"
        assert _unlink_plugin_frontend(plugin) is False


# ---------------------------------------------------------------------------
# cmd_link / cmd_unlink (integration-style with mocked subprocess)
# ---------------------------------------------------------------------------


class TestCmdLink:
    @patch("mld_sdk.link_command.subprocess.run", return_value=_ok_result())
    def test_link_full_flow(self, mock_run, workspace, monkeypatch):
        root, platform = workspace
        monkeypatch.chdir(platform)
        args = argparse.Namespace(sdk_path=None)
        cmd_link(args)

        # Should have called: bun link (register), uv pip install -e (drp),
        # bun link @morscherlab/mld-sdk (drp). mld-plugin-other has no .venv
        # or frontend, so only skip messages.
        commands = [call.args[0] for call in mock_run.call_args_list]
        assert ["bun", "link"] in commands
        assert any(
            c[0] == "uv" and c[1] == "pip" and "-e" in c for c in commands
        )
        assert ["bun", "link", "@morscherlab/mld-sdk"] in commands

    @patch("mld_sdk.link_command.subprocess.run", return_value=_ok_result())
    def test_link_with_sdk_path_override(self, mock_run, workspace, monkeypatch, tmp_path):
        _, platform = workspace
        monkeypatch.chdir(platform)

        custom_sdk = tmp_path / "custom-sdk"
        (custom_sdk / "packages" / "sdk-python").mkdir(parents=True)
        (custom_sdk / "packages" / "sdk-frontend").mkdir(parents=True)
        (custom_sdk / "packages" / "sdk-frontend" / "package.json").write_text("{}")

        args = argparse.Namespace(sdk_path=str(custom_sdk))
        cmd_link(args)

        # bun link should use the custom path
        bun_link_call = mock_run.call_args_list[0]
        assert bun_link_call.kwargs["cwd"] == custom_sdk / "packages" / "sdk-frontend"


class TestCmdUnlink:
    @patch("mld_sdk.link_command.subprocess.run", return_value=_ok_result())
    def test_unlink_full_flow(self, mock_run, workspace, monkeypatch):
        root, platform = workspace
        monkeypatch.chdir(platform)
        args = argparse.Namespace()
        cmd_unlink(args)

        commands = [call.args[0] for call in mock_run.call_args_list]
        # drp has .venv -> uv sync
        assert ["uv", "sync"] in commands
        # drp has frontend -> bun install
        assert ["bun", "install"] in commands

    @patch("mld_sdk.link_command.subprocess.run", return_value=_fail_result())
    def test_unlink_continues_on_failure(self, mock_run, workspace, monkeypatch):
        _, platform = workspace
        monkeypatch.chdir(platform)
        args = argparse.Namespace()
        # Should not raise even though all subprocess calls fail
        cmd_unlink(args)
