"""
Tests for kodara.remote.scanner — Remote Scan feature.

All tests are offline: git clone is mocked so no real network calls happen.

Run: pytest tests/test_remote_scan.py -v
"""

from __future__ import annotations

import shutil
import subprocess
import textwrap
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from kodara.remote.scanner import parse_url, scan_remote, ScanSummary


# ── parse_url ─────────────────────────────────────────────────────────────────


def test_parse_url_bare_github():
    """'github.com/user/repo' should become 'https://github.com/user/repo'."""
    url, is_remote = parse_url("github.com/user/repo")
    assert url == "https://github.com/user/repo"
    assert is_remote is True


def test_parse_url_https_unchanged():
    """Full https URL should be returned unchanged."""
    url, is_remote = parse_url("https://github.com/user/repo")
    assert url == "https://github.com/user/repo"
    assert is_remote is True


def test_parse_url_http_unchanged():
    """Full http URL should be returned unchanged."""
    url, is_remote = parse_url("http://github.com/user/repo")
    assert url == "http://github.com/user/repo"
    assert is_remote is True


def test_parse_url_relative_local():
    """'./local/path' should be detected as a local path."""
    url, is_remote = parse_url("./local/path")
    assert url == "./local/path"
    assert is_remote is False


def test_parse_url_parent_relative_local():
    """'../sibling' should be detected as local."""
    url, is_remote = parse_url("../sibling")
    assert is_remote is False


def test_parse_url_absolute_local():
    """/absolute/path should be detected as local."""
    url, is_remote = parse_url("/absolute/path/to/repo")
    assert is_remote is False


def test_parse_url_windows_drive():
    """C:\\path should be detected as local (Windows drive letter)."""
    url, is_remote = parse_url("C:\\Users\\user\\project")
    assert is_remote is False


def test_parse_url_with_trailing_whitespace():
    """Leading/trailing whitespace should be stripped."""
    url, is_remote = parse_url("  github.com/org/repo  ")
    assert url == "https://github.com/org/repo"
    assert is_remote is True


# ── scan_remote — local path ──────────────────────────────────────────────────


@pytest.fixture
def sample_python_project(tmp_path: Path) -> Path:
    """Create a minimal Python project for offline testing."""
    (tmp_path / "api").mkdir()
    (tmp_path / "auth").mkdir()

    (tmp_path / "main.py").write_text(textwrap.dedent("""
        from api.server import app
        from auth.middleware import verify_token

        def main():
            app.run()
    """))

    (tmp_path / "api" / "server.py").write_text(textwrap.dedent("""
        from auth.middleware import verify_token

        class APIServer:
            def run(self): pass
            def handle_request(self): pass
    """))

    (tmp_path / "auth" / "middleware.py").write_text(textwrap.dedent("""
        def verify_token(token: str) -> bool:
            return True
    """))

    return tmp_path


def test_scan_remote_local_path(sample_python_project: Path):
    """scan_remote on a local directory returns a valid ScanSummary."""
    summary = scan_remote(str(sample_python_project))

    assert isinstance(summary, ScanSummary)
    assert summary.file_count >= 3
    assert "python" in summary.language_counts
    assert summary.language_counts["python"] >= 3
    assert summary.scan_time_s >= 0
    assert isinstance(summary.top_modules, list)


def test_scan_remote_local_url_format(sample_python_project: Path):
    """scan_remote accepts './relative' style path when cwd matches."""
    # Use absolute path directly (resolve() handles the rest)
    summary = scan_remote(str(sample_python_project))
    assert summary.file_count > 0


def test_scan_summary_dataclass_fields(sample_python_project: Path):
    """ScanSummary has all required fields with correct types."""
    summary = scan_remote(str(sample_python_project))

    assert isinstance(summary.url, str)
    assert isinstance(summary.file_count, int)
    assert isinstance(summary.language_counts, dict)
    assert isinstance(summary.top_modules, list)
    assert isinstance(summary.scan_time_s, float)


# ── scan_remote — temp dir cleanup ───────────────────────────────────────────


def test_temp_dir_deleted_after_scan():
    """Temp directory created by clone is always deleted — tracked via mock."""
    captured_tmp = []

    original_mkdtemp = __import__("tempfile").mkdtemp

    def fake_mkdtemp(**kwargs):
        d = original_mkdtemp(**kwargs)
        captured_tmp.append(d)
        return d

    # We also need git clone to "succeed" without actually cloning
    def fake_run(cmd, **kwargs):
        if cmd[0] == "git" and cmd[1] == "--version":
            mock = MagicMock()
            mock.returncode = 0
            return mock
        if cmd[0] == "git" and cmd[1] == "clone":
            # Write a Python file into tmp_dir so scanner has something to parse
            tmp_dir = cmd[-1]
            Path(tmp_dir).mkdir(parents=True, exist_ok=True)
            (Path(tmp_dir) / "hello.py").write_text("def hello(): pass\n")
            mock = MagicMock()
            mock.returncode = 0
            mock.stderr = b""
            return mock
        return original_run(cmd, **kwargs)

    original_run = subprocess.run

    with patch("tempfile.mkdtemp", side_effect=fake_mkdtemp):
        with patch("subprocess.run", side_effect=fake_run):
            summary = scan_remote("github.com/fake/repo")

    assert len(captured_tmp) == 1
    tmp_path = captured_tmp[0]
    # The temp dir should be deleted
    assert not Path(tmp_path).exists(), f"Temp dir was not cleaned up: {tmp_path}"


def test_temp_dir_deleted_on_error():
    """Temp dir is cleaned up even when an error occurs during scan."""
    captured_tmp = []
    original_mkdtemp = __import__("tempfile").mkdtemp

    def fake_mkdtemp(**kwargs):
        d = original_mkdtemp(**kwargs)
        captured_tmp.append(d)
        return d

    def fake_run(cmd, **kwargs):
        if cmd[0] == "git" and cmd[1] == "--version":
            mock = MagicMock()
            mock.returncode = 0
            return mock
        if cmd[0] == "git" and cmd[1] == "clone":
            # Clone "succeeds" but leaves tmp_dir empty — scanner finds no files
            tmp_dir = cmd[-1]
            Path(tmp_dir).mkdir(parents=True, exist_ok=True)
            mock = MagicMock()
            mock.returncode = 0
            mock.stderr = b""
            return mock
        return subprocess.run(cmd, **kwargs)

    with patch("tempfile.mkdtemp", side_effect=fake_mkdtemp):
        with patch("subprocess.run", side_effect=fake_run):
            # Empty repo => scan succeeds with 0 files (no exception expected)
            summary = scan_remote("github.com/fake/empty")

    if captured_tmp:
        assert not Path(captured_tmp[0]).exists(), "Temp dir was not cleaned up after empty clone"


# ── git not installed → ValueError ───────────────────────────────────────────


def test_git_not_installed_raises_valueerror():
    """ValueError with clear message when git is not found."""
    def fake_run(cmd, **kwargs):
        if cmd[0] == "git":
            raise FileNotFoundError("git not found")
        return subprocess.run(cmd, **kwargs)

    with patch("subprocess.run", side_effect=fake_run):
        with pytest.raises(ValueError, match="git is not installed"):
            scan_remote("github.com/user/repo")


def test_git_clone_failure_raises_valueerror():
    """ValueError with informative message when git clone returns non-zero."""
    def fake_run(cmd, **kwargs):
        mock = MagicMock()
        if cmd[1] == "--version":
            mock.returncode = 0
            return mock
        if cmd[1] == "clone":
            mock.returncode = 128
            mock.stderr = b"Repository not found."
            return mock
        return subprocess.run(cmd, **kwargs)

    with patch("subprocess.run", side_effect=fake_run):
        with pytest.raises(ValueError, match="git clone failed"):
            scan_remote("github.com/non-existent/repo-xyz-12345")


# ── Regression: imports don't break existing modules ─────────────────────────


def test_existing_modules_still_importable():
    """Importing remote.scanner must not break core module imports."""
    from kodara.indexer.scanner import RepositoryScanner, ScanResult
    from kodara.graph.dependency import DependencyGraph
    from kodara.memory.store import MemoryStore, KodaraIndex
    from kodara.summarizer import generate_summaries
    from kodara.remote.scanner import scan_remote, parse_url, ScanSummary

    assert callable(scan_remote)
    assert callable(parse_url)
    assert RepositoryScanner is not None
    assert DependencyGraph is not None


def test_remote_scanner_module_exports():
    """Public API surface of remote.scanner is complete."""
    import kodara.remote.scanner as mod

    assert hasattr(mod, "ScanSummary")
    assert hasattr(mod, "parse_url")
    assert hasattr(mod, "scan_remote")
