# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""Tests for Sprint 10: Marketplace package format, validation, and installation."""

import json
import tempfile
from pathlib import Path
from unittest.mock import patch

# ── Test data ────────────────────────────────────────────────────────────────

VALID_JOB_PACKAGE = {
    "schema_version": "1.0",
    "type": "job_class",
    "metadata": {
        "id": "test_researcher",
        "name": "Research Scientist",
        "version": "1.0.0",
        "author": "Test Author",
        "description": "A research-focused job class for testing.",
        "requires_skills": ["websearch", "news"],
        "familiar_version": ">=1.14.52",
    },
    "job_class": {
        "key": "test_researcher",
        "name": "Research Scientist",
        "icon": "fa-microscope",
        "description": "Focused on research and analysis.",
        "prompt_fragment": "You are a research scientist. Focus on data and evidence.",
        "skill_weights": {"websearch": 1.5, "news": 1.2},
        "recommended_connects": ["searxng", "brave_search"],
        "proactive_focus": ["research updates", "paper summaries"],
    },
}

VALID_SKILL_PACKAGE = {
    "schema_version": "1.0",
    "type": "skill",
    "metadata": {
        "id": "test_calculator",
        "name": "Calculator",
        "version": "1.0.0",
        "author": "Test Author",
        "description": "Basic math operations.",
        "category": "utilities",
        "familiar_version": ">=1.14.52",
    },
    "tools": [
        {
            "name": "calculate",
            "description": "Perform a calculation.",
            "input_schema": {
                "type": "object",
                "properties": {
                    "expression": {"type": "string", "description": "Math expression"},
                },
                "required": ["expression"],
            },
            "category": "calculator",
        },
        {
            "name": "convert_units",
            "description": "Convert between units.",
            "input_schema": {
                "type": "object",
                "properties": {
                    "value": {"type": "number"},
                    "from_unit": {"type": "string"},
                    "to_unit": {"type": "string"},
                },
                "required": ["value", "from_unit", "to_unit"],
            },
            "category": "calculator",
        },
    ],
    "env_vars": [],
    "setup_instructions": "No setup required.",
}

VALID_BUNDLE = {
    "schema_version": "1.0",
    "type": "bundle",
    "metadata": {
        "id": "test_bundle",
        "name": "Test Bundle",
        "version": "1.0.0",
    },
    "job_class": VALID_JOB_PACKAGE["job_class"],
    "skills": [VALID_SKILL_PACKAGE],
}


# ── Job class marketplace tests ─────────────────────────────────────────────


class TestValidatePackage:
    def test_valid_package(self):
        from familiar.jobs._marketplace import validate_package

        errors = validate_package(VALID_JOB_PACKAGE)
        assert errors == []

    def test_missing_schema_version(self):
        from familiar.jobs._marketplace import validate_package

        pkg = {**VALID_JOB_PACKAGE, "schema_version": ""}
        errors = validate_package(pkg)
        assert any("schema_version" in e for e in errors)

    def test_wrong_schema_version(self):
        from familiar.jobs._marketplace import validate_package

        pkg = {**VALID_JOB_PACKAGE, "schema_version": "99.0"}
        errors = validate_package(pkg)
        assert any("Unsupported" in e for e in errors)

    def test_missing_type(self):
        from familiar.jobs._marketplace import validate_package

        pkg = {**VALID_JOB_PACKAGE, "type": ""}
        errors = validate_package(pkg)
        assert any("type" in e.lower() for e in errors)

    def test_invalid_type(self):
        from familiar.jobs._marketplace import validate_package

        pkg = {**VALID_JOB_PACKAGE, "type": "bogus"}
        errors = validate_package(pkg)
        assert any("Invalid type" in e for e in errors)

    def test_missing_metadata_fields(self):
        from familiar.jobs._marketplace import validate_package

        pkg = {**VALID_JOB_PACKAGE, "metadata": {"id": "test"}}
        errors = validate_package(pkg)
        assert any("name" in e for e in errors)
        assert any("version" in e for e in errors)

    def test_invalid_version_format(self):
        from familiar.jobs._marketplace import validate_package

        meta = {**VALID_JOB_PACKAGE["metadata"], "version": "1.0"}
        pkg = {**VALID_JOB_PACKAGE, "metadata": meta}
        errors = validate_package(pkg)
        assert any("version format" in e.lower() for e in errors)

    def test_missing_job_class(self):
        from familiar.jobs._marketplace import validate_package

        pkg = {**VALID_JOB_PACKAGE}
        del pkg["job_class"]
        errors = validate_package(pkg)
        assert any("job_class" in e for e in errors)

    def test_missing_job_class_fields(self):
        from familiar.jobs._marketplace import validate_package

        pkg = {**VALID_JOB_PACKAGE, "job_class": {"key": "test"}}
        errors = validate_package(pkg)
        assert any("prompt_fragment" in e for e in errors)


class TestCheckCompatibility:
    def test_compatible(self):
        from familiar.jobs._marketplace import check_compatibility

        result = check_compatibility(VALID_JOB_PACKAGE)
        assert result is None

    def test_no_constraint(self):
        from familiar.jobs._marketplace import check_compatibility

        pkg = {**VALID_JOB_PACKAGE, "metadata": {"id": "test", "name": "t", "version": "1.0.0"}}
        result = check_compatibility(pkg)
        assert result is None

    def test_incompatible(self):
        from familiar.jobs._marketplace import check_compatibility

        meta = {**VALID_JOB_PACKAGE["metadata"], "familiar_version": ">=99.0.0"}
        pkg = {**VALID_JOB_PACKAGE, "metadata": meta}
        result = check_compatibility(pkg)
        assert result is not None
        assert "99.0.0" in result


class TestInstallPackage:
    def test_install_job_class(self):
        from familiar.jobs._marketplace import install_package

        with tempfile.TemporaryDirectory() as tmpdir:
            with patch("familiar.jobs._marketplace.CUSTOM_DIR", Path(tmpdir)):
                result = install_package(VALID_JOB_PACKAGE)
        assert "Installed" in result
        assert "Research Scientist" in result

    def test_install_invalid(self):
        from familiar.jobs._marketplace import install_package

        result = install_package({"schema_version": "1.0"})
        assert "failed" in result.lower()

    def test_install_incompatible(self):
        from familiar.jobs._marketplace import install_package

        meta = {**VALID_JOB_PACKAGE["metadata"], "familiar_version": ">=99.0.0"}
        pkg = {**VALID_JOB_PACKAGE, "metadata": meta}
        result = install_package(pkg)
        assert "Compatibility" in result


class TestUninstallPackage:
    def test_uninstall(self):
        from familiar.jobs._marketplace import uninstall_package

        with tempfile.TemporaryDirectory() as tmpdir:
            target = Path(tmpdir) / "test_pkg.json"
            target.write_text("{}")
            with (
                patch("familiar.jobs._marketplace.CUSTOM_DIR", Path(tmpdir)),
                patch("familiar.jobs.JOB_CLASS_REGISTRY", {"test_pkg": None}),
            ):
                result = uninstall_package("test_pkg")
        assert "Uninstalled" in result

    def test_uninstall_missing(self):
        from familiar.jobs._marketplace import uninstall_package

        with tempfile.TemporaryDirectory() as tmpdir:
            with patch("familiar.jobs._marketplace.CUSTOM_DIR", Path(tmpdir)):
                result = uninstall_package("nonexistent")
        assert "not found" in result.lower()


class TestListPackages:
    def test_list_empty(self):
        from familiar.jobs._marketplace import list_packages

        with tempfile.TemporaryDirectory() as tmpdir:
            with patch("familiar.jobs._marketplace.CUSTOM_DIR", Path(tmpdir)):
                result = list_packages()
        assert result == []

    def test_list_with_packages(self):
        from familiar.jobs._marketplace import list_packages

        with tempfile.TemporaryDirectory() as tmpdir:
            p = Path(tmpdir) / "test_pkg.json"
            p.write_text(json.dumps(VALID_JOB_PACKAGE))
            with patch("familiar.jobs._marketplace.CUSTOM_DIR", Path(tmpdir)):
                result = list_packages()
        assert len(result) == 1
        assert result[0]["name"] == "Research Scientist"


class TestLoadPackageFile:
    def test_load_valid(self):
        from familiar.jobs._marketplace import load_package_file

        with tempfile.NamedTemporaryFile(suffix=".json", mode="w", delete=False) as f:
            json.dump(VALID_JOB_PACKAGE, f)
            f.flush()
            result = load_package_file(f.name)
        assert result["type"] == "job_class"

    def test_load_missing(self):
        from familiar.jobs._marketplace import load_package_file

        try:
            load_package_file("/tmp/nonexistent_pkg_12345.json")
            assert False, "Should have raised FileNotFoundError"
        except FileNotFoundError:
            pass


# ── Skill marketplace tests ─────────────────────────────────────────────────


class TestValidateSkillPackage:
    def test_valid(self):
        from familiar.skills._marketplace import validate_skill_package

        errors = validate_skill_package(VALID_SKILL_PACKAGE)
        assert errors == []

    def test_wrong_type(self):
        from familiar.skills._marketplace import validate_skill_package

        pkg = {**VALID_SKILL_PACKAGE, "type": "job_class"}
        errors = validate_skill_package(pkg)
        assert any("type" in e.lower() for e in errors)

    def test_no_tools(self):
        from familiar.skills._marketplace import validate_skill_package

        pkg = {**VALID_SKILL_PACKAGE, "tools": []}
        errors = validate_skill_package(pkg)
        assert any("No tools" in e for e in errors)

    def test_tool_missing_name(self):
        from familiar.skills._marketplace import validate_skill_package

        pkg = {
            **VALID_SKILL_PACKAGE,
            "tools": [{"description": "test", "input_schema": {"type": "object"}}],
        }
        errors = validate_skill_package(pkg)
        assert any("name" in e.lower() for e in errors)


class TestInstallSkillPackage:
    def test_install(self):
        from familiar.skills._marketplace import CUSTOM_SKILL_TOOLS, install_skill_package

        with tempfile.TemporaryDirectory() as tmpdir:
            with patch("familiar.skills._marketplace.CUSTOM_SKILLS_DIR", Path(tmpdir)):
                result = install_skill_package(VALID_SKILL_PACKAGE)
        assert "Installed" in result
        assert "2 tools" in result
        assert "test_calculator" in CUSTOM_SKILL_TOOLS
        # Clean up
        CUSTOM_SKILL_TOOLS.pop("test_calculator", None)

    def test_install_invalid(self):
        from familiar.skills._marketplace import install_skill_package

        result = install_skill_package({"type": "skill", "metadata": {}, "tools": []})
        assert "failed" in result.lower()


class TestUninstallSkillPackage:
    def test_uninstall(self):
        from familiar.skills._marketplace import uninstall_skill_package

        with tempfile.TemporaryDirectory() as tmpdir:
            target = Path(tmpdir) / "test_skill.json"
            target.write_text("{}")
            with patch("familiar.skills._marketplace.CUSTOM_SKILLS_DIR", Path(tmpdir)):
                result = uninstall_skill_package("test_skill")
        assert "Uninstalled" in result

    def test_uninstall_missing(self):
        from familiar.skills._marketplace import uninstall_skill_package

        with tempfile.TemporaryDirectory() as tmpdir:
            with patch("familiar.skills._marketplace.CUSTOM_SKILLS_DIR", Path(tmpdir)):
                result = uninstall_skill_package("nonexistent")
        assert "not found" in result.lower()


class TestListSkillPackages:
    def test_list(self):
        from familiar.skills._marketplace import list_skill_packages

        with tempfile.TemporaryDirectory() as tmpdir:
            p = Path(tmpdir) / "test_calc.json"
            p.write_text(json.dumps(VALID_SKILL_PACKAGE))
            with patch("familiar.skills._marketplace.CUSTOM_SKILLS_DIR", Path(tmpdir)):
                result = list_skill_packages()
        assert len(result) == 1
        assert result[0]["tools_count"] == 2


class TestGetAllCustomTools:
    def test_get_tools(self):
        from familiar.skills._marketplace import CUSTOM_SKILL_TOOLS, get_all_custom_tools

        CUSTOM_SKILL_TOOLS["test_a"] = [{"name": "tool1"}, {"name": "tool2"}]
        CUSTOM_SKILL_TOOLS["test_b"] = [{"name": "tool3"}]
        try:
            tools = get_all_custom_tools()
            assert len(tools) == 3
            names = {t["name"] for t in tools}
            assert names == {"tool1", "tool2", "tool3"}
        finally:
            CUSTOM_SKILL_TOOLS.pop("test_a", None)
            CUSTOM_SKILL_TOOLS.pop("test_b", None)


# ── Bundle test ──────────────────────────────────────────────────────────────


class TestBundleInstall:
    def test_install_bundle(self):
        from familiar.jobs._marketplace import install_package
        from familiar.skills._marketplace import CUSTOM_SKILL_TOOLS

        with tempfile.TemporaryDirectory() as tmpdir:
            with (
                patch("familiar.jobs._marketplace.CUSTOM_DIR", Path(tmpdir)),
                patch(
                    "familiar.skills._marketplace.CUSTOM_SKILLS_DIR",
                    Path(tmpdir) / "skills",
                ),
            ):
                result = install_package(VALID_BUNDLE)
        assert "test_researcher" in result
        assert "Calculator" in result
        # Clean up
        CUSTOM_SKILL_TOOLS.pop("test_calculator", None)
