# 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 auto-pilot onboarding: service catalog, auto-setup engine,
onboarding profile/credentials steps, and setup skill."""

from __future__ import annotations

import os
from unittest.mock import patch

from familiar.channels.connect_wizard.service_catalog import (
    SERVICE_CATALOG,
    ServiceEntry,
    get_paste_key_services,
    get_service,
    get_services_for_job_class,
)

# ── TestServiceCatalog ───────────────────────────────────────────────


class TestServiceCatalog:
    """Tests for the centralized service catalog."""

    def test_catalog_not_empty(self):
        assert len(SERVICE_CATALOG) >= 50

    def test_all_service_display_keys_have_entries(self):
        from familiar.channels.connect_wizard._helpers import SERVICE_DISPLAY

        for key in SERVICE_DISPLAY:
            assert key in SERVICE_CATALOG, f"SERVICE_DISPLAY key '{key}' missing from catalog"

    def test_entries_have_required_fields(self):
        for key, entry in SERVICE_CATALOG.items():
            assert isinstance(entry, ServiceEntry)
            assert entry.key == key
            assert entry.display_name
            assert entry.category in ("llm", "integrations", "apis", "selfhosted", "security")
            assert entry.credential_type in (
                "single_token",
                "url_and_token",
                "multi_field",
                "oauth",
                "local",
            )
            assert entry.setup_complexity in (
                "paste_key",
                "url_and_key",
                "multi_step",
                "oauth_flow",
                "local_install",
            )

    def test_get_service_found(self):
        entry = get_service("todoist")
        assert entry is not None
        assert entry.key == "todoist"
        assert "TODOIST_API_TOKEN" in entry.env_vars

    def test_get_service_not_found(self):
        assert get_service("nonexistent_service_xyz") is None

    def test_get_services_for_job_class_all_classes(self):
        for job_class in [
            "helper",
            "social_worker",
            "business_buddy",
            "nonprofit_director",
            "chef",
            "artist",
        ]:
            services = get_services_for_job_class(job_class)
            assert len(services) > 0, f"No services for job class '{job_class}'"

    def test_get_services_for_job_class_chef_includes_spoonacular(self):
        services = get_services_for_job_class("chef")
        keys = [s.key for s in services]
        assert "spoonacular" in keys
        assert "mealie" in keys

    def test_get_services_for_job_class_sorted_by_complexity(self):
        services = get_services_for_job_class("business_buddy")
        complexities = [s.setup_complexity for s in services]
        order = {
            "paste_key": 0,
            "url_and_key": 1,
            "multi_step": 2,
            "oauth_flow": 3,
            "local_install": 4,
        }
        numeric = [order.get(c, 99) for c in complexities]
        assert numeric == sorted(numeric)

    def test_get_paste_key_services(self):
        services = get_paste_key_services()
        assert len(services) > 0
        for s in services:
            assert s.credential_type == "single_token"
            assert len(s.env_vars) > 0

    def test_stripe_uses_correct_env_var(self):
        entry = get_service("stripe")
        assert entry is not None
        assert "STRIPE_SECRET_KEY" in entry.env_vars
        assert "STRIPE_API_KEY" not in entry.env_vars

    def test_prerequisites_have_catalog_entries(self):
        from familiar.core.skills import SKILL_PREREQUISITES

        # Known mappings where skill key differs from catalog key
        skill_to_catalog = {"stripe_pay": "stripe"}
        # Skills that are library-only prerequisites (no API credentials)
        library_only = {
            "voice",
            "transcription",
            "gpio",
            "browser",
            "gdrive",
            "calendar",
            "proton",
            "meetings",
            "triage",
            "notifications",
            "messaging",
            "triggers",
        }

        for skill_key in SKILL_PREREQUISITES:
            if skill_key in library_only:
                continue
            catalog_key = skill_to_catalog.get(skill_key, skill_key)
            assert catalog_key in SERVICE_CATALOG, (
                f"SKILL_PREREQUISITES['{skill_key}'] has no SERVICE_CATALOG entry "
                f"(expected key '{catalog_key}')"
            )

    def test_paste_key_services_excludes_multi_field(self):
        services = get_paste_key_services()
        keys = [s.key for s in services]
        # Reddit requires client_id + client_secret — should not be paste_key
        assert "reddit" not in keys
        assert "email" not in keys


# ── TestAutoSetupEngine ──────────────────────────────────────────────


class TestAutoSetupEngine:
    """Tests for the auto-setup engine."""

    def test_get_setup_status_empty(self, tmp_path):
        from familiar.channels.connect_wizard.auto_setup import AutoSetupEngine

        engine = AutoSetupEngine(data_dir=tmp_path)
        status = engine.get_setup_status()
        assert isinstance(status, dict)
        assert "todoist" in status
        assert status["todoist"]["configured"] is False

    def test_apply_credentials_writes_env(self, tmp_path):
        from familiar.channels.connect_wizard.auto_setup import AutoSetupEngine

        engine = AutoSetupEngine(data_dir=tmp_path)
        results = engine.apply_credentials(
            {
                "todoist": {"TODOIST_API_TOKEN": "test_token_123"},
            }
        )
        assert results["todoist"]["saved"] is True
        assert results["todoist"]["error"] is None

        # Verify .env was written
        env_content = (tmp_path / ".env").read_text()
        assert "TODOIST_API_TOKEN=test_token_123" in env_content

    def test_apply_credentials_skips_empty(self, tmp_path):
        from familiar.channels.connect_wizard.auto_setup import AutoSetupEngine

        engine = AutoSetupEngine(data_dir=tmp_path)
        results = engine.apply_credentials(
            {
                "todoist": {"TODOIST_API_TOKEN": ""},
            }
        )
        assert results["todoist"]["saved"] is False
        assert "No credentials" in results["todoist"]["error"]

    def test_apply_credentials_unknown_service(self, tmp_path):
        from familiar.channels.connect_wizard.auto_setup import AutoSetupEngine

        engine = AutoSetupEngine(data_dir=tmp_path)
        results = engine.apply_credentials(
            {
                "fake_service": {"FAKE_KEY": "value"},
            }
        )
        assert results["fake_service"]["saved"] is False
        assert "Unknown" in results["fake_service"]["error"]

    def test_get_setup_status_detects_configured(self, tmp_path):
        from familiar.channels.connect_wizard.auto_setup import AutoSetupEngine

        # Write a .env file
        env_path = tmp_path / ".env"
        env_path.write_text("TODOIST_API_TOKEN=abc123\n")

        engine = AutoSetupEngine(data_dir=tmp_path)
        status = engine.get_setup_status()
        assert status["todoist"]["configured"] is True
        assert status["todoist"]["env_vars"]["TODOIST_API_TOKEN"] is True

    def test_test_service_no_test_url(self, tmp_path):
        from familiar.channels.connect_wizard.auto_setup import AutoSetupEngine

        engine = AutoSetupEngine(data_dir=tmp_path)
        ok, msg = engine.test_service("ollama")
        assert ok is True
        assert "No connectivity test" in msg or "No credentials" in msg

    def test_test_service_unknown(self, tmp_path):
        from familiar.channels.connect_wizard.auto_setup import AutoSetupEngine

        engine = AutoSetupEngine(data_dir=tmp_path)
        ok, msg = engine.test_service("nonexistent")
        assert ok is False
        assert "Unknown" in msg

    def test_test_service_missing_token(self, tmp_path):
        from familiar.channels.connect_wizard.auto_setup import AutoSetupEngine

        engine = AutoSetupEngine(data_dir=tmp_path)
        # Ensure env var is not set
        os.environ.pop("TODOIST_API_TOKEN", None)
        ok, msg = engine.test_service("todoist")
        assert ok is False
        assert "not set" in msg

    def test_save_profile(self, tmp_path):
        from familiar.channels.connect_wizard.auto_setup import AutoSetupEngine

        engine = AutoSetupEngine(data_dir=tmp_path)
        engine.save_profile(
            {
                "org_name": "Test Org",
                "org_type": "nonprofit",
                "owner_email": "test@example.com",
            }
        )

        import yaml

        config = yaml.safe_load((tmp_path / "config.yaml").read_text())
        assert config["organization"]["org_name"] == "Test Org"
        assert config["organization"]["org_type"] == "nonprofit"

    def test_get_recommended_services(self, tmp_path):
        from familiar.channels.connect_wizard.auto_setup import AutoSetupEngine

        engine = AutoSetupEngine(data_dir=tmp_path)
        services = engine.get_recommended_services("chef")
        keys = [s.key for s in services]
        assert "spoonacular" in keys


# ── TestOnboardingProfile ────────────────────────────────────────────


class TestOnboardingProfile:
    """Tests for the onboarding engine profile/credentials steps."""

    def test_validate_profile_saves_state(self, tmp_path):
        from familiar.onboard_engine import OnboardingEngine

        engine = OnboardingEngine(project_dir=tmp_path)
        result = engine.validate_profile(
            {
                "org_name": "Sunrise Shelter",
                "org_type": "nonprofit",
            }
        )
        assert result.success is True
        assert result.data["profile"]["org_name"] == "Sunrise Shelter"

    def test_validate_profile_returns_recommendations(self, tmp_path):
        from familiar.onboard_engine import OnboardingEngine

        engine = OnboardingEngine(project_dir=tmp_path)
        result = engine.validate_profile(
            {
                "org_name": "Test",
                "job_class": "chef",
            }
        )
        assert result.success is True
        assert isinstance(result.data.get("recommended_services"), list)

    def test_validate_credentials_applies(self, tmp_path):
        from familiar.onboard_engine import OnboardingEngine

        engine = OnboardingEngine(project_dir=tmp_path)
        result = engine.validate_credentials(
            {
                "credentials": {
                    "todoist": {"TODOIST_API_TOKEN": "onboard_test_123"},
                },
                "skip_test": True,
            }
        )
        assert result.success is True
        assert result.data["results"]["todoist"]["saved"] is True

    def test_validate_credentials_empty_skips(self, tmp_path):
        from familiar.onboard_engine import OnboardingEngine

        engine = OnboardingEngine(project_dir=tmp_path)
        result = engine.validate_credentials({})
        assert result.success is True
        assert result.data.get("skipped") is True

    def test_validate_credentials_does_not_break_flow(self, tmp_path):
        """Skipping credentials should not prevent later steps."""
        from familiar.onboard_engine import OnboardingEngine

        engine = OnboardingEngine(project_dir=tmp_path)
        # Skip profile
        r1 = engine.validate_profile({})
        assert r1.success is True
        # Skip credentials
        r2 = engine.validate_credentials({})
        assert r2.success is True
        # Briefing still works
        r3 = engine.validate_briefing({"enabled": False})
        assert r3.success is True


# ── TestSetupSkill ───────────────────────────────────────────────────


class TestSetupSkill:
    """Tests for the setup skill tools."""

    def test_check_setup_status_returns_string(self):
        from familiar.skills.setup.skill import check_setup_status

        result = check_setup_status({})
        assert isinstance(result, str)
        assert "Configured" in result

    def test_check_setup_status_with_job_class(self):
        from familiar.skills.setup.skill import check_setup_status

        result = check_setup_status({"job_class": "chef"})
        assert isinstance(result, str)

    def test_auto_configure_service_missing_service(self):
        from familiar.skills.setup.skill import auto_configure_service

        result = auto_configure_service({"credentials": {"KEY": "val"}})
        assert "Error" in result
        assert "required" in result

    def test_auto_configure_service_unknown_service(self):
        from familiar.skills.setup.skill import auto_configure_service

        result = auto_configure_service(
            {
                "service": "nonexistent_xyz",
                "credentials": {"KEY": "val"},
            }
        )
        assert "Error" in result
        assert "Unknown" in result

    def test_auto_configure_service_missing_credentials(self):
        from familiar.skills.setup.skill import auto_configure_service

        result = auto_configure_service({"service": "todoist"})
        assert "Error" in result
        assert "credentials" in result.lower()

    def test_tools_list_structure(self):
        from familiar.skills.setup.skill import TOOLS

        assert isinstance(TOOLS, list)
        assert len(TOOLS) == 2
        names = [t["name"] for t in TOOLS]
        assert "check_setup_status" in names
        assert "auto_configure_service" in names
        for tool in TOOLS:
            assert "handler" in tool
            assert callable(tool["handler"])
            assert "input_schema" in tool
            assert "description" in tool

    @patch.dict(os.environ, {"TODOIST_API_TOKEN": "skill_test_token"}, clear=False)
    def test_auto_configure_writes_and_tests(self, tmp_path):
        from familiar.channels.connect_wizard.auto_setup import AutoSetupEngine
        from familiar.skills.setup.skill import auto_configure_service

        # Patch AutoSetupEngine constructor to use tmp_path
        orig_init = AutoSetupEngine.__init__

        def _patched_init(self_eng, data_dir=None):
            orig_init(self_eng, data_dir=tmp_path)

        with patch.object(AutoSetupEngine, "__init__", _patched_init):
            result = auto_configure_service(
                {
                    "service": "todoist",
                    "credentials": {"TODOIST_API_TOKEN": "skill_test_token"},
                }
            )
            # Should succeed in saving (test may fail without network)
            assert "Todoist" in result
