# 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

"""Unit tests for the Tarot Card Onboarding System."""

import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))

from familiar.channels.connect_wizard._tarot import (
    ARCANA_ORDER,
    SPECIES_TAROT_VOICE,
    TAROT_ARCANA,
    format_card_reveal,
    format_spread_message,
    get_tarot_spread,
    get_unconfigured_spread,
)
from familiar.channels.formatting import FormatMode  # noqa: E402
from familiar.creature.species import SPECIES_ORDER  # noqa: E402

# ── Fixtures ─────────────────────────────────────────────────────────


def _all_status_false() -> dict[str, bool]:
    """Service status with everything unconfigured."""
    keys = set()
    for arcana in TAROT_ARCANA.values():
        keys.update(arcana["services"])
    return {k: False for k in keys}


def _all_status_true() -> dict[str, bool]:
    """Service status with everything configured."""
    keys = set()
    for arcana in TAROT_ARCANA.values():
        keys.update(arcana["services"])
    return {k: True for k in keys}


# ── Arcana coverage tests ────────────────────────────────────────────


def test_tarot_arcana_covers_all_services():
    """Every service from _get_service_status appears in exactly one arcana."""
    # Collect all services mentioned across arcana
    arcana_services: list[str] = []
    for arcana in TAROT_ARCANA.values():
        arcana_services.extend(arcana["services"])

    # No duplicates across arcana
    assert len(arcana_services) == len(set(arcana_services)), (
        f"Duplicate services across arcana: "
        f"{[s for s in arcana_services if arcana_services.count(s) > 1]}"
    )

    # Cross-check: build the service status dict to find its keys
    # (we can't call _get_service_status without an agent, so build it manually)
    expected_keys = {
        "anthropic",
        "openai",
        "gemini",
        "ollama",
        "google",
        "calendar",
        "email",
        "sms",
        "browser",
        "voice",
        "websearch",
        "healthcare",
        "github",
        "slack",
        "jellyfin",
        "gitea",
        "homeassistant",
        "joplin",
        "pihole",
        "nextcloud",
        "proton",
        "email_server",
        "vaultwarden",
        "encryption",
        "phi_detection",
        "rbac",
        "user_management",
    }
    arcana_set = set(arcana_services)

    # Every expected key should be in some arcana
    missing = expected_keys - arcana_set
    assert not missing, f"Services not in any arcana: {missing}"

    # Every arcana service should be in the expected keys
    extra = arcana_set - expected_keys
    assert not extra, f"Arcana services not in service status: {extra}"


def test_arcana_order_matches_keys():
    """ARCANA_ORDER contains exactly the keys from TAROT_ARCANA."""
    assert set(ARCANA_ORDER) == set(TAROT_ARCANA.keys())
    assert len(ARCANA_ORDER) == len(TAROT_ARCANA)


# ── Spread logic tests ──────────────────────────────────────────────


def test_spread_returns_all_arcana():
    """get_tarot_spread returns an entry for each arcana."""
    status = _all_status_false()
    spread = get_tarot_spread(status)
    assert len(spread) == len(TAROT_ARCANA)
    keys = [card["key"] for card in spread]
    assert keys == ARCANA_ORDER


def test_spread_counts_configured():
    """Configured services are counted correctly."""
    status = _all_status_false()
    status["anthropic"] = True
    status["openai"] = True
    spread = get_tarot_spread(status)
    mind = next(c for c in spread if c["key"] == "mind")
    assert mind["configured"] == 2
    assert mind["total"] == 4
    assert mind["pending"] == 2


def test_spread_excludes_fully_configured():
    """get_unconfigured_spread omits arcana with all services configured."""
    status = _all_status_false()
    # Configure all mind services
    for svc in TAROT_ARCANA["mind"]["services"]:
        status[svc] = True
    spread = get_unconfigured_spread(status)
    keys = [card["key"] for card in spread]
    assert "mind" not in keys
    # Others should still be present
    assert "voice" in keys


def test_spread_empty_when_all_configured():
    """Returns empty spread when everything is configured."""
    status = _all_status_true()
    spread = get_unconfigured_spread(status)
    assert spread == []


def test_format_spread_all_configured():
    """When all configured, format_spread_message returns allclear message."""
    status = _all_status_true()
    spread = get_tarot_spread(status)
    text, options = format_spread_message(spread, "fox", FormatMode.PLAIN)
    assert "fully attuned" in text
    assert options == []


def test_format_spread_has_options():
    """When unconfigured services exist, spread has card options + Done."""
    status = _all_status_false()
    spread = get_tarot_spread(status)
    text, options = format_spread_message(spread, "owl", FormatMode.PLAIN)
    assert "ancient patterns" in text  # Owl intro
    # Should have options for each arcana + Done
    assert len(options) == len(ARCANA_ORDER) + 1
    assert options[-1][0] == "tarot_done"


def test_format_spread_skips_done_arcana_in_pending_count():
    """Fully configured arcana shows 'done' rather than pending count."""
    status = _all_status_false()
    for svc in TAROT_ARCANA["mind"]["services"]:
        status[svc] = True
    spread = get_tarot_spread(status)
    text, options = format_spread_message(spread, "fox", FormatMode.PLAIN)
    # Mind card should show done
    mind_option = next(o for o in options if "tarot_card_mind" in o[0])
    assert "\u2705" in mind_option[1]


# ── Species voice tests ──────────────────────────────────────────────


def test_species_voice_all_species():
    """Every species in SPECIES_ORDER has tarot voice entries."""
    for species in SPECIES_ORDER:
        assert species in SPECIES_TAROT_VOICE, f"Missing tarot voice for {species}"
        voice = SPECIES_TAROT_VOICE[species]
        for key in ("intro", "reveal", "skip", "done", "allclear"):
            assert key in voice, f"Missing voice key '{key}' for {species}"


def test_species_voice_unique_intros():
    """Each species has a unique intro line."""
    intros = [v["intro"] for v in SPECIES_TAROT_VOICE.values()]
    assert len(intros) == len(set(intros))


# ── Card reveal tests ───────────────────────────────────────────────


def test_card_reveal_shows_status():
    """Configured services get check, unconfigured get empty square."""
    status = _all_status_false()
    status["anthropic"] = True
    text, options = format_card_reveal("mind", status, "fox", FormatMode.PLAIN)
    assert "\u2705" in text  # Anthropic configured
    assert "\u2b1c" in text  # Others unconfigured
    assert "The Mind" in text


def test_card_reveal_has_configure_buttons():
    """All services get configure/manage buttons (configured gets ✅, unconfigured ⚙️)."""
    status = _all_status_false()
    status["anthropic"] = True
    text, options = format_card_reveal("mind", status, "fox", FormatMode.PLAIN)
    # All 4 mind services get buttons + Back
    configure_opts = [o for o in options if o[0].startswith("tarot_configure_")]
    assert len(configure_opts) == 4  # anthropic (✅), openai, gemini, ollama (⚙️)
    # Configured service gets ✅ prefix
    anthropic_btn = [o for o in configure_opts if "anthropic" in o[0]]
    assert anthropic_btn and "\u2705" in anthropic_btn[0][1]
    # Unconfigured services get ⚙️ prefix
    openai_btn = [o for o in configure_opts if "openai" in o[0]]
    assert openai_btn and "\u2699\ufe0f" in openai_btn[0][1]
    back_opts = [o for o in options if o[0] == "tarot_back"]
    assert len(back_opts) == 1


def test_card_reveal_unknown_arcana():
    """Unknown arcana key returns error text."""
    text, options = format_card_reveal("nonexistent", {}, "fox", FormatMode.PLAIN)
    assert "Unknown" in text
    assert options == []


# ── Callback key tests ──────────────────────────────────────────────


def test_tarot_callback_keys_format():
    """All tarot callback keys follow expected patterns."""
    status = _all_status_false()
    spread = get_tarot_spread(status)
    _, options = format_spread_message(spread, "fox", FormatMode.PLAIN)

    for key, label in options:
        assert key.startswith("tarot_"), f"Bad key: {key}"

    # Card keys
    card_keys = [k for k, _ in options if k.startswith("tarot_card_")]
    assert len(card_keys) == len(ARCANA_ORDER)

    # Each card key matches an arcana
    for ck in card_keys:
        arcana_key = ck[len("tarot_card_") :]
        assert arcana_key in TAROT_ARCANA


def test_card_reveal_configure_keys():
    """Configure buttons use tarot_configure_ prefix with valid inner keys."""
    status = _all_status_false()
    _, options = format_card_reveal("mind", status, "fox", FormatMode.PLAIN)
    configure_opts = [k for k, _ in options if k.startswith("tarot_configure_")]
    # Should map to real service button callback keys
    from familiar.channels.connect_wizard._helpers import SERVICE_BUTTON_LABELS

    valid_cb_keys = {v[0] for v in SERVICE_BUTTON_LABELS.values()}
    for ck in configure_opts:
        inner = ck[len("tarot_configure_") :]
        assert inner in valid_cb_keys, f"Invalid inner key: {inner}"
