"""Tests for alive.sharing — export, import, and browse."""

import json
import tarfile
from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
from threading import Thread

import pytest

from alive.sharing.browse import CommunitySkill, browse_skills
from alive.sharing.export import ExportResult, _validate_skill_dir, export_skill
from alive.sharing.import_skill import (
    ImportResult,
    SkillPreview,
    _extract_preview,
    _safe_extract,
    import_skill,
    preview_skill,
)


# ── Helpers ──────────────────────────────────────────────────────────────────


def _make_skill(skills_dir: Path, name: str = "test_skill") -> Path:
    """Create a minimal valid skill directory."""
    skill_dir = skills_dir / name
    skill_dir.mkdir(parents=True)
    (skill_dir / "skill.py").write_text(
        'def run(**kwargs):\n    return {"result": "ok"}\n'
    )
    (skill_dir / "metadata.json").write_text(json.dumps({
        "name": name,
        "description": f"A test skill: {name}",
        "dependencies": [],
        "created_at": "2026-01-01T00:00:00+00:00",
        "times_used": 0,
        "success_count": 0,
        "fail_count": 0,
    }))
    (skill_dir / "test_skill.py").write_text(
        "from skill import run\n\ndef test_runs():\n    assert run() == {'result': 'ok'}\n"
    )
    (skill_dir / "SKILL.md").write_text(f"# {name}\n\nA test skill.\n")
    return skill_dir


def _make_archive(tmp_path: Path, skills_dir: Path, name: str = "test_skill") -> Path:
    """Create a .tar.gz archive from a skill directory."""
    skill_dir = skills_dir / name
    archive_path = tmp_path / f"{name}.alive-skill.tar.gz"
    with tarfile.open(archive_path, "w:gz") as tar:
        tar.add(str(skill_dir), arcname=name)
    return archive_path


# ── Export tests ─────────────────────────────────────────────────────────────


class TestExport:
    def test_export_creates_archive(self, tmp_path):
        skills_dir = tmp_path / "skills"
        _make_skill(skills_dir, "my_skill")

        result = export_skill("my_skill", output_dir=tmp_path, skills_dir=skills_dir)

        assert isinstance(result, ExportResult)
        assert result.archive_path.exists()
        assert result.archive_path.name == "my_skill.alive-skill.tar.gz"
        assert result.skill_name == "my_skill"
        assert "alive install" in result.install_command
        assert result.gist_url is None

    def test_export_archive_contents(self, tmp_path):
        skills_dir = tmp_path / "skills"
        _make_skill(skills_dir, "my_skill")

        result = export_skill("my_skill", output_dir=tmp_path, skills_dir=skills_dir)

        with tarfile.open(result.archive_path, "r:gz") as tar:
            names = tar.getnames()
            basenames = {n.split("/", 1)[1] if "/" in n else n for n in names}
            assert "skill.py" in basenames
            assert "metadata.json" in basenames

    def test_export_nonexistent_skill(self, tmp_path):
        skills_dir = tmp_path / "skills"
        skills_dir.mkdir()
        with pytest.raises(FileNotFoundError, match="not found"):
            export_skill("nope", skills_dir=skills_dir)

    def test_export_missing_required_files(self, tmp_path):
        skills_dir = tmp_path / "skills"
        skill_dir = skills_dir / "broken"
        skill_dir.mkdir(parents=True)
        (skill_dir / "skill.py").write_text("x = 1")
        # Missing metadata.json

        with pytest.raises(ValueError, match="missing required files"):
            export_skill("broken", skills_dir=skills_dir)

    def test_validate_skill_dir(self, tmp_path):
        skills_dir = tmp_path / "skills"
        _make_skill(skills_dir, "good")
        # Should not raise
        _validate_skill_dir(skills_dir / "good", "good")

    def test_validate_skill_dir_missing(self, tmp_path):
        skill_dir = tmp_path / "empty"
        skill_dir.mkdir()
        with pytest.raises(ValueError, match="missing required files"):
            _validate_skill_dir(skill_dir, "empty")


# ── Import tests ─────────────────────────────────────────────────────────────


class TestImport:
    def test_preview_skill(self, tmp_path):
        skills_dir = tmp_path / "skills"
        _make_skill(skills_dir, "preview_me")
        archive = _make_archive(tmp_path, skills_dir, "preview_me")

        preview = preview_skill(str(archive))

        assert isinstance(preview, SkillPreview)
        assert preview.name == "preview_me"
        assert "skill.py" in preview.files
        assert "metadata.json" in preview.files
        assert "def run" in preview.files["skill.py"]

    def test_preview_invalid_archive(self, tmp_path):
        bad_file = tmp_path / "bad.tar.gz"
        bad_file.write_text("not a tar")
        with pytest.raises(ValueError, match="Invalid archive"):
            preview_skill(str(bad_file))

    def test_preview_missing_skill_py(self, tmp_path):
        """Archive without skill.py should fail validation."""
        skill_dir = tmp_path / "source" / "no_skill"
        skill_dir.mkdir(parents=True)
        (skill_dir / "metadata.json").write_text('{"name": "no_skill"}')

        archive = tmp_path / "no_skill.tar.gz"
        with tarfile.open(archive, "w:gz") as tar:
            tar.add(str(skill_dir), arcname="no_skill")

        with pytest.raises(ValueError, match="missing skill.py"):
            preview_skill(str(archive))

    def test_import_local_archive(self, tmp_path):
        src_skills = tmp_path / "src_skills"
        _make_skill(src_skills, "importable")
        archive = _make_archive(tmp_path, src_skills, "importable")

        dest_skills = tmp_path / "dest_skills"
        dest_skills.mkdir()

        result = import_skill(str(archive), skills_dir=dest_skills, run_tests=False)

        assert isinstance(result, ImportResult)
        assert result.skill_name == "importable"
        assert result.installed is True
        assert (dest_skills / "importable" / "skill.py").exists()

    def test_import_conflict(self, tmp_path):
        skills_dir = tmp_path / "skills"
        _make_skill(skills_dir, "existing")
        archive = _make_archive(tmp_path, skills_dir, "existing")

        with pytest.raises(ValueError, match="already exists"):
            import_skill(str(archive), skills_dir=skills_dir)

    def test_import_nonexistent_source(self):
        with pytest.raises(FileNotFoundError, match="not found"):
            import_skill("/nonexistent/path.tar.gz")

    def test_safe_extract_rejects_path_traversal(self, tmp_path):
        """Archive with path traversal should be rejected."""
        archive = tmp_path / "evil.tar.gz"
        evil_dir = tmp_path / "evil_src"
        evil_dir.mkdir()
        (evil_dir / "good.txt").write_text("fine")

        with tarfile.open(archive, "w:gz") as tar:
            tar.add(str(evil_dir / "good.txt"), arcname="../../../etc/evil.txt")

        with tarfile.open(archive, "r:gz") as tar:
            with pytest.raises(ValueError, match="path traversal"):
                _safe_extract(tar, tmp_path / "dest")

    def test_extract_preview(self, tmp_path):
        skills_dir = tmp_path / "skills"
        _make_skill(skills_dir, "previewable")
        archive = _make_archive(tmp_path, skills_dir, "previewable")

        preview = _extract_preview(archive)
        assert preview.name == "previewable"
        assert preview.description == "A test skill: previewable"
        assert preview.dependencies == []
        assert "skill.py" in preview.files

    def test_extract_preview_empty_archive(self, tmp_path):
        archive = tmp_path / "empty.tar.gz"
        with tarfile.open(archive, "w:gz"):
            pass  # empty archive
        with pytest.raises(ValueError, match="empty"):
            _extract_preview(archive)


# ── Browse tests ─────────────────────────────────────────────────────────────


class TestBrowse:
    def test_browse_parses_catalog(self, tmp_path):
        catalog = {
            "skills": [
                {
                    "name": "web_scraper",
                    "description": "Scrape web pages",
                    "author": "alice",
                    "tags": ["web", "scraping"],
                    "install_url": "https://example.com/web_scraper.tar.gz",
                    "downloads": 42,
                },
                {
                    "name": "csv_parser",
                    "description": "Parse CSV files",
                    "author": "bob",
                    "tags": ["data", "csv"],
                    "install_url": "https://example.com/csv_parser.tar.gz",
                },
            ]
        }
        catalog_file = tmp_path / "catalog.json"
        catalog_file.write_text(json.dumps(catalog))

        skills = browse_skills(catalog_url=f"file://{catalog_file}")

        assert len(skills) == 2
        assert skills[0].name == "web_scraper"
        assert skills[0].author == "alice"
        assert skills[0].downloads == 42
        assert skills[1].name == "csv_parser"

    def test_browse_with_search(self, tmp_path):
        catalog = {
            "skills": [
                {"name": "web_scraper", "description": "Scrape web pages", "tags": ["web"]},
                {"name": "csv_parser", "description": "Parse CSV files", "tags": ["data"]},
            ]
        }
        catalog_file = tmp_path / "catalog.json"
        catalog_file.write_text(json.dumps(catalog))

        skills = browse_skills(search="web", catalog_url=f"file://{catalog_file}")

        assert len(skills) == 1
        assert skills[0].name == "web_scraper"

    def test_browse_empty_catalog(self, tmp_path):
        catalog_file = tmp_path / "catalog.json"
        catalog_file.write_text(json.dumps({"skills": []}))

        skills = browse_skills(catalog_url=f"file://{catalog_file}")
        assert skills == []

    def test_browse_search_no_match(self, tmp_path):
        catalog = {
            "skills": [
                {"name": "web_scraper", "description": "Scrape", "tags": ["web"]},
            ]
        }
        catalog_file = tmp_path / "catalog.json"
        catalog_file.write_text(json.dumps(catalog))

        skills = browse_skills(search="database", catalog_url=f"file://{catalog_file}")
        assert skills == []

    def test_browse_bad_url(self):
        with pytest.raises(ConnectionError):
            browse_skills(catalog_url="file:///nonexistent/catalog.json")

    def test_community_skill_dataclass(self):
        s = CommunitySkill(
            name="test",
            description="A test",
            author="me",
            tags=["t1"],
            downloads=10,
        )
        assert s.name == "test"
        assert s.downloads == 10


# ── URL import tests ─────────────────────────────────────────────────────────


class TestURLImport:
    def test_import_from_url(self, tmp_path):
        """Test importing a skill from a local HTTP server."""
        # Create archive
        src_skills = tmp_path / "src"
        _make_skill(src_skills, "url_skill")
        archive = _make_archive(tmp_path, src_skills, "url_skill")

        # Serve it on a local HTTP server
        serve_dir = tmp_path / "serve"
        serve_dir.mkdir()
        import shutil
        shutil.copy2(str(archive), str(serve_dir / "url_skill.tar.gz"))

        handler = SimpleHTTPRequestHandler
        server = HTTPServer(("127.0.0.1", 0), handler)
        port = server.server_address[1]
        thread = Thread(target=server.serve_forever, daemon=True)
        thread.start()

        try:
            import os
            orig_cwd = os.getcwd()
            os.chdir(str(serve_dir))

            dest = tmp_path / "dest"
            dest.mkdir()
            result = import_skill(
                f"http://127.0.0.1:{port}/url_skill.tar.gz",
                skills_dir=dest,
                run_tests=False,
            )
            assert result.installed is True
            assert result.skill_name == "url_skill"
            assert (dest / "url_skill" / "skill.py").exists()
        finally:
            os.chdir(orig_cwd)
            server.shutdown()


# ── Backward compatibility ───────────────────────────────────────────────────


class TestBackwardCompat:
    def test_skills_py_export(self, tmp_path):
        """alive.skills.export_skill still works."""
        from alive.skills import export_skill as old_export

        skills_dir = tmp_path / "skills"
        _make_skill(skills_dir, "compat_skill")

        # Monkey-patch SKILLS_DIR temporarily
        import alive.config as cfg
        original = cfg.SKILLS_DIR
        cfg.SKILLS_DIR = skills_dir
        try:
            path = old_export("compat_skill", output_path=tmp_path / "out.alive-skill.tar.gz")
            assert path.exists()
        finally:
            cfg.SKILLS_DIR = original

    def test_skills_py_import(self, tmp_path):
        """alive.skills.import_skill still works."""
        from alive.skills import import_skill as old_import

        src = tmp_path / "src"
        _make_skill(src, "compat_imp")
        archive = _make_archive(tmp_path, src, "compat_imp")

        dest = tmp_path / "dest"
        dest.mkdir()

        import alive.config as cfg
        original = cfg.SKILLS_DIR
        cfg.SKILLS_DIR = dest
        try:
            name = old_import(str(archive))
            assert name == "compat_imp"
            assert (dest / "compat_imp" / "skill.py").exists()
        finally:
            cfg.SKILLS_DIR = original
