"""Tests for Hive Mode (F-021) - GPU pooling.

Covers: HiveManager, KarmaEngine, PrivacyClassifier, HiveRouter, persistence, CLI.
"""

from __future__ import annotations

import json
from datetime import datetime, timezone
from typing import TYPE_CHECKING

import pytest

from llmhosts.hive.karma import KarmaEngine
from llmhosts.hive.manager import HiveManager
from llmhosts.hive.models import HiveMember, HiveRole
from llmhosts.hive.privacy import PrivacyClassifier
from llmhosts.hive.router import HiveRouter

if TYPE_CHECKING:
    from pathlib import Path

    pass


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture()
def hive_dir(tmp_path: Path) -> Path:
    """Temporary directory for Hive storage (shared for create/join)."""
    d = tmp_path / ".llmhosts"
    d.mkdir()
    return d


@pytest.fixture()
def manager(hive_dir: Path) -> HiveManager:
    """HiveManager pointing at a temporary directory (owner)."""
    registry = hive_dir / "hive_registry"
    return HiveManager(hive_dir, registry_path=registry)


@pytest.fixture()
def manager_b(hive_dir: Path) -> HiveManager:
    """Second HiveManager (simulates another user: own data_dir, shared registry)."""
    bob_dir = hive_dir / "bob"
    registry = hive_dir / "hive_registry"
    return HiveManager(bob_dir, registry_path=registry)


# ---------------------------------------------------------------------------
# HiveManager: create
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
@pytest.mark.unit
async def test_create_hive(manager: HiveManager) -> None:
    """Creating a Hive should add owner as first member."""
    await manager.initialize()
    hive = await manager.create_hive("My Hive", "Alice")

    assert hive.id
    assert hive.name == "My Hive"
    assert hive.invite_code
    assert len(hive.invite_code) >= 6
    assert len(hive.members) == 1
    assert hive.members[0].name == "Alice"
    assert hive.members[0].role == HiveRole.OWNER
    assert hive.members[0].karma_score == 1.0


# ---------------------------------------------------------------------------
# HiveManager: join
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
@pytest.mark.unit
async def test_join_hive(manager: HiveManager, manager_b: HiveManager) -> None:
    """Joining with valid invite code should add member."""
    await manager.initialize()
    hive = await manager.create_hive("Shared Hive", "Alice")

    await manager_b.initialize()
    member = await manager_b.join_hive(hive.invite_code, "Bob")

    assert member.name == "Bob"
    assert member.role == HiveRole.MEMBER
    assert member.karma_score == 1.0

    hive2 = await manager_b.get_hive()
    assert hive2 is not None
    assert len(hive2.members) == 2


@pytest.mark.asyncio
@pytest.mark.unit
async def test_join_hive_invalid_code(manager_b: HiveManager) -> None:
    """Joining with invalid invite code should raise ValueError."""
    await manager_b.initialize()

    with pytest.raises(ValueError, match="Invalid or expired invite code"):
        await manager_b.join_hive("INVALID1", "Bob")


# ---------------------------------------------------------------------------
# HiveManager: leave
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
@pytest.mark.unit
async def test_leave_hive(manager: HiveManager, manager_b: HiveManager) -> None:
    """Member should be able to leave."""
    await manager.initialize()
    hive = await manager.create_hive("Leave Test", "Alice")

    await manager_b.initialize()
    await manager_b.join_hive(hive.invite_code, "Bob")

    left = await manager_b.leave_hive()
    assert left is True

    hive_after = await manager_b.get_hive()
    assert hive_after is None


@pytest.mark.asyncio
@pytest.mark.unit
async def test_owner_cannot_leave(manager: HiveManager) -> None:
    """Owner trying to leave should raise ValueError."""
    await manager.initialize()
    await manager.create_hive("Owner Hive", "Alice")

    with pytest.raises(ValueError, match="Owner cannot leave"):
        await manager.leave_hive()


# ---------------------------------------------------------------------------
# HiveManager: max members
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
@pytest.mark.unit
async def test_hive_max_members(manager_b: HiveManager, hive_dir: Path) -> None:
    """Hive should enforce max_members limit."""
    registry = hive_dir / "hive_registry"
    mgr_small = HiveManager(hive_dir, registry_path=registry)
    await mgr_small.initialize()
    hive = await mgr_small.create_hive("Small Hive", "Alice")
    hive.max_members = 2
    await mgr_small.save()
    await mgr_small._publish_to_registry(hive)

    # Bob joins - ok
    await manager_b.initialize()
    await manager_b.join_hive(hive.invite_code, "Bob")

    # Carol tries to join - need a third manager with own dir, shared registry
    carol_dir = hive_dir / "carol"
    mgr_c = HiveManager(carol_dir, registry_path=registry)
    await mgr_c.initialize()
    with pytest.raises(ValueError, match="full"):
        await mgr_c.join_hive(hive.invite_code, "Carol")


# ---------------------------------------------------------------------------
# HiveManager: invite
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
@pytest.mark.unit
async def test_generate_invite(manager: HiveManager) -> None:
    """generate_invite should return HiveInvite with code."""
    await manager.initialize()
    await manager.create_hive("Invite Hive", "Alice")

    inv = await manager.generate_invite()
    assert inv.hive_id
    assert inv.invite_code
    assert inv.created_by


@pytest.mark.asyncio
@pytest.mark.unit
async def test_generate_invite_without_hive(manager: HiveManager) -> None:
    """generate_invite without Hive should raise ValueError."""
    await manager.initialize()

    with pytest.raises(ValueError, match="Not in a Hive"):
        await manager.generate_invite()


# ---------------------------------------------------------------------------
# KarmaEngine
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
@pytest.mark.unit
async def test_karma_contribution(manager: HiveManager) -> None:
    """Recording contribution should increase karma."""
    await manager.initialize()
    hive = await manager.create_hive("Karma Hive", "Alice")
    owner_id = hive.members[0].id

    karma = KarmaEngine(manager)
    new_karma = await karma.record_contribution(owner_id, 2.0)
    assert new_karma > 1.0  # contributed > consumed


@pytest.mark.asyncio
@pytest.mark.unit
async def test_karma_consumption(manager: HiveManager) -> None:
    """Recording consumption should decrease karma."""
    await manager.initialize()
    hive = await manager.create_hive("Karma Hive", "Alice")
    owner_id = hive.members[0].id

    karma = KarmaEngine(manager)
    await karma.record_contribution(owner_id, 1.0)
    new_karma = await karma.record_consumption(owner_id, 2.0)
    assert new_karma < 1.0


@pytest.mark.unit
def test_karma_ratio_clamped() -> None:
    """Karma should be clamped to [0.1, 10.0]."""
    assert KarmaEngine.calculate_karma(0, 1) == 0.1  # Zero contributed -> min
    assert KarmaEngine.calculate_karma(1, 0) == 10.0  # Zero consumed -> max
    assert KarmaEngine.calculate_karma(100, 1) == 10.0  # High ratio clamped
    assert KarmaEngine.calculate_karma(0.01, 1) == 0.1  # Low ratio clamped


@pytest.mark.asyncio
@pytest.mark.unit
async def test_karma_priority_order(manager: HiveManager, manager_b: HiveManager) -> None:
    """get_priority_order should return members sorted by karma (highest first)."""
    await manager.initialize()
    hive = await manager.create_hive("Order Hive", "Alice")
    alice_id = hive.members[0].id

    await manager_b.initialize()
    bob = await manager_b.join_hive(hive.invite_code, "Bob")
    bob_id = bob.id

    # Use manager_b which has the full hive (both members)
    karma = KarmaEngine(manager_b)
    await karma.record_contribution(alice_id, 5.0)
    await karma.record_consumption(bob_id, 2.0)

    order = await karma.get_priority_order()
    assert len(order) == 2
    assert order[0][1] >= order[1][1]


# ---------------------------------------------------------------------------
# PrivacyClassifier
# ---------------------------------------------------------------------------


@pytest.mark.unit
def test_privacy_classify_private_pii() -> None:
    """PII in prompt should classify as PRIVATE."""
    classifier = PrivacyClassifier()
    r = classifier.classify("My email is alice@example.com and my phone is 555-123-4567")
    assert r.tier.value == "private"
    assert r.pii_detected is True


@pytest.mark.unit
def test_privacy_classify_standard() -> None:
    """Clean prompt should classify as STANDARD."""
    classifier = PrivacyClassifier()
    r = classifier.classify("What is the capital of France?")
    assert r.tier.value == "standard"
    assert r.pii_detected is False


@pytest.mark.unit
def test_privacy_classify_credentials() -> None:
    """API keys in prompt should classify as PRIVATE."""
    classifier = PrivacyClassifier()
    r = classifier.classify("Use this key: sk-proj-abc123def456ghi789jkl012mno345pqr")
    assert r.tier.value == "private"
    assert r.pii_detected is True


@pytest.mark.unit
def test_privacy_classify_sensitive_marker() -> None:
    """Explicit [PRIVATE] marker should classify as PRIVATE."""
    classifier = PrivacyClassifier()
    r = classifier.classify("[PRIVATE] Secret document contents")
    assert r.tier.value == "private"


# ---------------------------------------------------------------------------
# HiveRouter
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
@pytest.mark.unit
async def test_hive_router_private_stays_local(manager: HiveManager) -> None:
    """PRIVATE prompt should never use Hive (stays local)."""
    await manager.initialize()
    await manager.create_hive("Test Hive", "Alice")

    from llmhosts.hive.privacy import PrivacyClassifier

    router = HiveRouter(manager, PrivacyClassifier(), KarmaEngine(manager))
    decision = await router.route("My SSN is 123-45-6789", "llama3.2")

    assert decision.use_hive is False
    assert decision.privacy_tier.value == "private"
    assert "local" in decision.reason.lower()


@pytest.mark.asyncio
@pytest.mark.unit
async def test_hive_router_standard_uses_hive(manager: HiveManager, manager_b: HiveManager) -> None:
    """STANDARD prompt with multiple members should route to Hive."""
    await manager.initialize()
    hive = await manager.create_hive("Route Hive", "Alice")

    await manager_b.initialize()
    await manager_b.join_hive(hive.invite_code, "Bob")

    from llmhosts.hive.privacy import PrivacyClassifier

    router = HiveRouter(manager_b, PrivacyClassifier(), KarmaEngine(manager_b))
    decision = await router.route("What is 2+2?", "llama3.2")

    assert decision.use_hive is True
    assert decision.target_member_id is not None
    assert decision.privacy_tier.value == "standard"


# ---------------------------------------------------------------------------
# Persistence
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
@pytest.mark.unit
async def test_hive_persistence(hive_dir: Path) -> None:
    """Hive should survive save/load round-trip."""
    mgr1 = HiveManager(hive_dir)
    await mgr1.initialize()
    hive = await mgr1.create_hive("Persistent Hive", "Alice")

    mgr2 = HiveManager(hive_dir)
    await mgr2.initialize()
    hive2 = await mgr2.get_hive()

    assert hive2 is not None
    assert hive2.name == hive.name
    assert hive2.id == hive.id
    assert len(hive2.members) == 1
    assert hive2.members[0].name == "Alice"


@pytest.mark.asyncio
@pytest.mark.unit
async def test_hive_persistence_json_valid(hive_dir: Path) -> None:
    """hive.json should be valid JSON with expected structure."""
    mgr = HiveManager(hive_dir)
    await mgr.initialize()
    await mgr.create_hive("JSON Hive", "Alice")

    hive_file = hive_dir / "hive.json"
    assert hive_file.is_file()
    data = json.loads(hive_file.read_text(encoding="utf-8"))
    assert "hive" in data
    assert data["hive"]["name"] == "JSON Hive"
    assert "current_member_id" in data


# ---------------------------------------------------------------------------
# HiveMember defaults
# ---------------------------------------------------------------------------


@pytest.mark.unit
def test_hive_member_defaults() -> None:
    """HiveMember should have sensible defaults."""
    now = datetime.now(tz=timezone.utc)
    m = HiveMember(
        id="test-id",
        name="Test",
        role=HiveRole.MEMBER,
        joined_at=now,
    )
    assert m.email is None
    assert m.last_seen is None
    assert m.devices == []
    assert m.karma_score == 1.0
    assert m.compute_contributed_hours == 0.0
    assert m.compute_consumed_hours == 0.0
    assert m.active is True
    assert m.sharing_enabled is True
    assert m.sharing_schedule is None
    assert m.shared_models == []


# ---------------------------------------------------------------------------
# Hive status karma display
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
@pytest.mark.unit
async def test_hive_status_karma_display(manager: HiveManager) -> None:
    """Current member karma and hours are available for CLI 'hive status' display."""
    await manager.initialize()
    hive = await manager.create_hive("Karma Display Hive", "Alice")
    current_member_id = manager.get_current_member_id()
    assert current_member_id is not None
    member = next((m for m in hive.members if m.id == current_member_id), None)
    assert member is not None
    assert member.karma_score == 1.0
    assert member.compute_contributed_hours == 0.0
    assert member.compute_consumed_hours == 0.0

    karma = KarmaEngine(manager)
    await karma.record_contribution(current_member_id, 2.0)
    await karma.record_consumption(current_member_id, 0.5)
    hive2 = await manager.get_hive()
    assert hive2 is not None
    member2 = next((m for m in hive2.members if m.id == current_member_id), None)
    assert member2 is not None
    assert member2.karma_score > 1.0
    assert member2.compute_contributed_hours == 2.0
    assert member2.compute_consumed_hours == 0.5
