"""Tests for alive.core.self_model."""

import json

import pytest

from alive.core.self_model import (
    GapRecord,
    SelfModel,
    _build_growth_timeline,
    _relative_time,
    build_self_model,
    classify_skill_keyword,
    classify_skills_llm,
    display_self_model,
    list_gaps,
    record_gap,
    resolve_gap,
)
from alive.core.skill_registry import SkillInfo, SkillRegistry
from alive.llm.base import LLMProvider


# ── Mock provider ──────────────────────────────────────────────────────────


class MockProvider(LLMProvider):
    """LLM provider that returns canned responses."""

    name = "mock"

    def __init__(self, responses: list[str] | None = None):
        self._responses = list(responses or [])
        self._call_count = 0

    async def complete(self, system_prompt: str, user_prompt: str) -> str:
        if self._call_count < len(self._responses):
            response = self._responses[self._call_count]
        else:
            response = "{}"
        self._call_count += 1
        return response


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


@pytest.fixture
def registry(tmp_path):
    """Create a registry pointing at a temp directory (no seed skills)."""
    import alive.seeds._installer as installer

    original = installer._SEEDS_DIR
    installer._SEEDS_DIR = tmp_path / "empty_seeds"
    installer._SEEDS_DIR.mkdir()
    reg = SkillRegistry(skills_dir=tmp_path / "skills")
    installer._SEEDS_DIR = original
    return reg


@pytest.fixture
def db_path(tmp_path):
    return tmp_path / "test.db"


# ── Data structures ─────────────────────────────────────────────────────────


def test_gap_record_defaults():
    gap = GapRecord(id=1, request="parse PDF", error="failed", timestamp="2026-01-01T00:00:00+00:00")
    assert gap.resolved is False
    assert gap.resolved_by == ""


def test_self_model_creation():
    model = SelfModel(total_skills=5, total_executions=10, overall_success_rate=0.8)
    assert model.total_skills == 5
    assert model.total_executions == 10
    assert model.overall_success_rate == 0.8
    assert model.domains == {}
    assert model.gaps == []
    assert model.growth_timeline == {}


# ── SQLite gap CRUD ──────────────────────────────────────────────────────────


def test_record_gap(db_path):
    gap = record_gap("parse PDF files", "no library found", db_path=db_path)
    assert gap.id == 1
    assert gap.request == "parse PDF files"
    assert gap.error == "no library found"
    assert gap.resolved is False


def test_list_gaps(db_path):
    record_gap("parse PDF", "failed", db_path=db_path)
    record_gap("send email", "smtp error", db_path=db_path)
    gaps = list_gaps(db_path=db_path)
    assert len(gaps) == 2
    # Most recent first
    assert gaps[0].request == "send email"
    assert gaps[1].request == "parse PDF"


def test_resolve_gap(db_path):
    gap = record_gap("parse PDF", "failed", db_path=db_path)
    resolve_gap(gap.id, "pdf_parser", db_path=db_path)
    # Unresolved list should be empty
    gaps = list_gaps(db_path=db_path)
    assert len(gaps) == 0
    # Include resolved
    all_gaps = list_gaps(include_resolved=True, db_path=db_path)
    assert len(all_gaps) == 1
    assert all_gaps[0].resolved is True
    assert all_gaps[0].resolved_by == "pdf_parser"


def test_list_gaps_excludes_resolved(db_path):
    record_gap("task A", "error", db_path=db_path)
    gap_b = record_gap("task B", "error", db_path=db_path)
    resolve_gap(gap_b.id, "skill_b", db_path=db_path)
    gaps = list_gaps(db_path=db_path)
    assert len(gaps) == 1
    assert gaps[0].request == "task A"


def test_list_gaps_empty_db(db_path):
    gaps = list_gaps(db_path=db_path)
    assert gaps == []


def test_list_gaps_nonexistent_db(tmp_path):
    gaps = list_gaps(db_path=tmp_path / "nonexistent.db")
    assert gaps == []


# ── Keyword classification ──────────────────────────────────────────────────


def test_classify_web_skill():
    skill = SkillInfo(name="web_fetch", description="Fetch content from a URL")
    assert classify_skill_keyword(skill) == "web"


def test_classify_file_skill():
    skill = SkillInfo(name="file_reader", description="Read a file from disk")
    assert classify_skill_keyword(skill) == "file"


def test_classify_system_skill():
    skill = SkillInfo(name="shell_run", description="Run a shell command")
    assert classify_skill_keyword(skill) == "system"


def test_classify_math_skill():
    skill = SkillInfo(name="fibonacci", description="Calculate fibonacci numbers")
    assert classify_skill_keyword(skill) == "math"


def test_classify_unknown_skill():
    skill = SkillInfo(name="xyzzy", description="Does something mysterious")
    assert classify_skill_keyword(skill) == "other"


# ── LLM classification ──────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_classify_skills_llm_success():
    provider = MockProvider(responses=[
        '{"web_fetch": "web", "file_reader": "file"}'
    ])
    skills = [
        SkillInfo(name="web_fetch", description="Fetch URL"),
        SkillInfo(name="file_reader", description="Read file"),
    ]
    result = await classify_skills_llm(skills, provider)
    assert result == {"web_fetch": "web", "file_reader": "file"}


@pytest.mark.asyncio
async def test_classify_skills_llm_fallback_on_error():
    """LLM returns garbage — should return empty dict, not crash."""
    provider = MockProvider(responses=["not valid json at all"])
    skills = [SkillInfo(name="x", description="y")]
    result = await classify_skills_llm(skills, provider)
    assert result == {}


@pytest.mark.asyncio
async def test_classify_skills_llm_filters_invalid_domains():
    provider = MockProvider(responses=[
        '{"skill_a": "web", "skill_b": "invalid_domain"}'
    ])
    skills = [
        SkillInfo(name="skill_a", description="a"),
        SkillInfo(name="skill_b", description="b"),
    ]
    result = await classify_skills_llm(skills, provider)
    assert result == {"skill_a": "web"}


@pytest.mark.asyncio
async def test_classify_skills_llm_empty():
    provider = MockProvider()
    result = await classify_skills_llm([], provider)
    assert result == {}


# ── build_self_model ────────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_build_self_model_empty(registry, db_path):
    model = await build_self_model(registry=registry, db_path=db_path)
    assert model.total_skills == 0
    assert model.total_executions == 0
    assert model.overall_success_rate == 0.0
    assert model.domains == {}
    assert model.gaps == []


@pytest.mark.asyncio
async def test_build_self_model_with_skills(registry, db_path):
    registry.register(
        name="web_fetch",
        code='def run(): return {}',
        description="Fetch content from a URL",
    )
    registry.register(
        name="file_reader",
        code='def run(): return {}',
        description="Read a file from disk",
    )
    # Simulate some usage
    registry.update_stats("web_fetch", success=True)
    registry.update_stats("web_fetch", success=True)
    registry.update_stats("file_reader", success=False)

    model = await build_self_model(registry=registry, db_path=db_path)
    assert model.total_skills == 2
    assert model.total_executions == 3
    assert model.overall_success_rate == pytest.approx(2 / 3, rel=0.01)

    # Domain classification should have happened (keyword-based)
    assert "web" in model.domains
    assert "file" in model.domains


@pytest.mark.asyncio
async def test_build_self_model_with_gaps(registry, db_path):
    record_gap("parse PDF", "no library", db_path=db_path)
    model = await build_self_model(registry=registry, db_path=db_path)
    assert len(model.gaps) == 1
    assert model.gaps[0].request == "parse PDF"


@pytest.mark.asyncio
async def test_build_self_model_with_llm_classification(registry, db_path):
    """LLM classification is used when provider is given."""
    registry.register(
        name="my_skill",
        code='def run(): return {}',
        description="Does web stuff",
    )
    provider = MockProvider(responses=['{"my_skill": "web"}'])
    model = await build_self_model(registry=registry, provider=provider, db_path=db_path)
    assert "web" in model.domains
    assert model.domains["web"][0].name == "my_skill"


# ── Confidence bucketing ─────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_confidence_buckets(registry, db_path):
    # High confidence: many successes
    registry.register(name="strong", code='def run(): return {}', description="strong")
    for _ in range(20):
        registry.update_stats("strong", success=True)

    # Medium confidence: untested (0.5)
    registry.register(name="medium", code='def run(): return {}', description="medium")

    # Low confidence: failures
    registry.register(name="weak", code='def run(): return {}', description="weak")
    for _ in range(5):
        registry.update_stats("weak", success=False)

    model = await build_self_model(registry=registry, db_path=db_path)
    assert any(s.name == "strong" for s in model.high_confidence)
    assert any(s.name == "medium" for s in model.medium_confidence)
    assert any(s.name == "weak" for s in model.low_confidence)


@pytest.mark.asyncio
async def test_confidence_buckets_empty(registry, db_path):
    model = await build_self_model(registry=registry, db_path=db_path)
    assert model.high_confidence == []
    assert model.medium_confidence == []
    assert model.low_confidence == []


# ── Growth timeline ──────────────────────────────────────────────────────────


def test_growth_timeline_multiple_weeks():
    skills = [
        SkillInfo(name="a", description="a", created_at="2026-03-10T00:00:00+00:00"),
        SkillInfo(name="b", description="b", created_at="2026-03-11T00:00:00+00:00"),
        SkillInfo(name="c", description="c", created_at="2026-03-18T00:00:00+00:00"),
    ]
    timeline = _build_growth_timeline(skills)
    assert len(timeline) == 2
    # 2026-03-10 and 2026-03-11 are in the same week
    values = list(timeline.values())
    assert values[0] == 2  # first week: 2 skills
    assert values[1] == 1  # second week: 1 skill


def test_growth_timeline_no_dates():
    skills = [SkillInfo(name="a", description="a")]
    timeline = _build_growth_timeline(skills)
    assert timeline == {}


# ── Relative time ──────────────────────────────────────────────────────────


def test_relative_time_invalid():
    assert _relative_time("") == "unknown"
    assert _relative_time("not-a-date") == "unknown"


# ── Display ──────────────────────────────────────────────────────────────────


def test_display_self_model_no_crash():
    """display_self_model should not crash with any valid model."""
    from rich.console import Console

    model = SelfModel(
        total_skills=2,
        total_executions=5,
        overall_success_rate=0.8,
        high_confidence=[SkillInfo(name="a", description="good")],
        medium_confidence=[],
        low_confidence=[SkillInfo(name="b", description="weak")],
        domains={"web": [SkillInfo(name="a", description="good")]},
        gaps=[GapRecord(id=1, request="parse PDF", error="failed", timestamp="2026-03-18T00:00:00+00:00")],
        growth_timeline={"2026-W12": 2},
    )
    display_self_model(model, console=Console(quiet=True))


def test_display_self_model_empty():
    """display_self_model should handle empty model."""
    from rich.console import Console

    model = SelfModel(total_skills=0, total_executions=0, overall_success_rate=0.0)
    display_self_model(model, console=Console(quiet=True))


# ── Additional self-model tests ─────────────────────────────────────────────

from alive.core.self_model import (
    RequestRecord,
    _bar,
    _classify_and_persist,
    _ensure_db,
    list_requests,
    record_request,
)


# ── _bar function ────────────────────────────────────────────────────────────


def test_bar_full():
    """Full bar when count equals max."""
    bar = _bar(10, 10, width=10)
    assert bar == "█" * 10


def test_bar_empty():
    """Empty bar when count is 0."""
    bar = _bar(0, 10, width=10)
    assert bar == "░" * 10


def test_bar_half():
    """Half bar."""
    bar = _bar(5, 10, width=10)
    assert "█" in bar
    assert "░" in bar
    assert len(bar) == 10


def test_bar_zero_max():
    """max_count=0 should return all empty."""
    bar = _bar(0, 0, width=10)
    assert bar == "░" * 10


def test_bar_default_width():
    """Default width should be 20."""
    bar = _bar(5, 10)
    assert len(bar) == 20


# ── _ensure_db ──────────────────────────────────────────────────────────────


def test_ensure_db_creates_tables(tmp_path):
    """_ensure_db should create both gaps and requests tables."""
    db = tmp_path / "test.db"
    conn = _ensure_db(db)
    try:
        # Verify tables exist by querying them
        conn.execute("SELECT count(*) FROM gaps")
        conn.execute("SELECT count(*) FROM requests")
    finally:
        conn.close()


def test_ensure_db_idempotent(tmp_path):
    """Calling _ensure_db twice should not raise."""
    db = tmp_path / "test.db"
    conn1 = _ensure_db(db)
    conn1.close()
    conn2 = _ensure_db(db)
    conn2.close()


def test_ensure_db_creates_parent_dirs(tmp_path):
    """_ensure_db should create parent directories if needed."""
    db = tmp_path / "deep" / "nested" / "test.db"
    conn = _ensure_db(db)
    conn.close()
    assert db.exists()


# ── _classify_and_persist ──────────────────────────────────────────────────


def test_classify_and_persist_keyword(registry):
    """Unclassified skills should get keyword-based domain classification."""
    registry.register(name="web_thing", code='def run(): return {}', description="Fetch a URL")
    skills = registry.list_all()
    assert skills[0].domain == ""

    _classify_and_persist(skills, registry)

    assert skills[0].domain == "web"
    # Check persistence
    import json
    meta = json.loads((registry._skills_dir / "web_thing" / "metadata.json").read_text())
    assert meta["domain"] == "web"


def test_classify_and_persist_llm_override(registry):
    """LLM results should take precedence over keyword classification."""
    registry.register(name="ambiguous", code='def run(): return {}', description="Something")
    skills = registry.list_all()

    _classify_and_persist(skills, registry, llm_results={"ambiguous": "data"})

    assert skills[0].domain == "data"


def test_classify_and_persist_skips_already_classified(registry):
    """Skills with existing domain should not be reclassified."""
    registry.register(name="classified", code='def run(): return {}', description="test")
    skill = registry.get_skill("classified")
    skill.domain = "math"
    registry._save_metadata(skill)

    skills = registry.list_all()
    _classify_and_persist(skills, registry, llm_results={"classified": "web"})

    # Should still be "math", not overwritten
    assert skills[0].domain == "math"


# ── classify_skill_keyword edge cases ───────────────────────────────────────


def test_classify_text_skill():
    skill = SkillInfo(name="summarizer", description="Summarize and format text")
    assert classify_skill_keyword(skill) == "text"


def test_classify_data_skill():
    skill = SkillInfo(name="sql_query", description="Query a database with SQL")
    assert classify_skill_keyword(skill) == "data"


def test_classify_empty_name_and_desc():
    """Empty name and description should return 'other'."""
    skill = SkillInfo(name="", description="")
    assert classify_skill_keyword(skill) == "other"


# ── Request logging edge cases ──────────────────────────────────────────────


def test_record_request_returns_record(db_path):
    rec = record_request("hello", "success", db_path=db_path)
    assert isinstance(rec, RequestRecord)
    assert rec.id == 1
    assert rec.request_text == "hello"
    assert rec.status == "success"
    assert rec.skills_used == []
    assert rec.skills_built == []


def test_record_request_with_skills(db_path):
    rec = record_request(
        "do stuff", "success",
        skills_used=["skill_a", "skill_b"],
        skills_built=["new_skill"],
        db_path=db_path,
    )
    assert rec.skills_used == ["skill_a", "skill_b"]
    assert rec.skills_built == ["new_skill"]


def test_list_requests_ordering(db_path):
    """Most recent request should appear first."""
    record_request("first", "success", db_path=db_path)
    record_request("second", "failed", db_path=db_path)
    record_request("third", "success", db_path=db_path)

    requests = list_requests(db_path=db_path)
    assert len(requests) == 3
    assert requests[0].request_text == "third"
    assert requests[2].request_text == "first"


# ── _relative_time edge cases ───────────────────────────────────────────────


def test_relative_time_recent():
    """Recent timestamp should return 'just now' or hours."""
    from datetime import datetime, timezone

    now = datetime.now(timezone.utc).isoformat()
    result = _relative_time(now)
    assert result in ("just now",) or "h ago" in result


def test_relative_time_yesterday():
    """Timestamp from yesterday should return 'yesterday'."""
    from datetime import datetime, timedelta, timezone

    yesterday = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
    result = _relative_time(yesterday)
    assert result == "yesterday"


def test_relative_time_days_ago():
    """Timestamp from 3 days ago should return '3d ago'."""
    from datetime import datetime, timedelta, timezone

    dt = (datetime.now(timezone.utc) - timedelta(days=3)).isoformat()
    result = _relative_time(dt)
    assert result == "3d ago"


def test_relative_time_weeks_ago():
    """Timestamp from 14 days ago should return '2 weeks ago'."""
    from datetime import datetime, timedelta, timezone

    dt = (datetime.now(timezone.utc) - timedelta(days=14)).isoformat()
    result = _relative_time(dt)
    assert result == "2 weeks ago"


def test_relative_time_one_week_ago():
    """Timestamp from 7 days ago should return '1 week ago'."""
    from datetime import datetime, timedelta, timezone

    dt = (datetime.now(timezone.utc) - timedelta(days=7)).isoformat()
    result = _relative_time(dt)
    assert result == "1 week ago"


# ── Growth timeline edge cases ──────────────────────────────────────────────


def test_growth_timeline_invalid_date():
    """Skills with invalid created_at should be skipped."""
    skills = [
        SkillInfo(name="a", description="a", created_at="not-a-date"),
        SkillInfo(name="b", description="b", created_at="2026-03-10T00:00:00+00:00"),
    ]
    timeline = _build_growth_timeline(skills)
    assert len(timeline) == 1


def test_growth_timeline_none_created_at():
    """Skills with empty created_at should be skipped."""
    skills = [
        SkillInfo(name="a", description="a", created_at=""),
        SkillInfo(name="b", description="b"),
    ]
    timeline = _build_growth_timeline(skills)
    assert timeline == {}


# ── build_self_model edge cases ────────────────────────────────────────────


@pytest.mark.asyncio
async def test_build_self_model_success_rate_calc(registry, db_path):
    """Success rate should be correct with mixed results."""
    registry.register(name="s", code='def run(): return {}', description="test")
    for _ in range(3):
        registry.update_stats("s", success=True)
    registry.update_stats("s", success=False)

    model = await build_self_model(registry=registry, db_path=db_path)
    assert model.total_executions == 4
    assert model.overall_success_rate == pytest.approx(0.75, rel=0.01)


@pytest.mark.asyncio
async def test_build_self_model_domain_grouping(registry, db_path):
    """Skills should be grouped by domain."""
    registry.register(name="web_a", code='def run(): return {}', description="Fetch URL")
    registry.register(name="web_b", code='def run(): return {}', description="Scrape web page")
    registry.register(name="math_a", code='def run(): return {}', description="Calculate fibonacci numbers")

    model = await build_self_model(registry=registry, db_path=db_path)
    assert "web" in model.domains
    assert len(model.domains["web"]) == 2
    assert "math" in model.domains


# ── Gap truncation ──────────────────────────────────────────────────────────


def test_record_gap_truncates_long_error(db_path):
    """Error strings longer than 2000 chars should be truncated."""
    long_error = "x" * 5000
    gap = record_gap("test", long_error, db_path=db_path)
    assert len(gap.error) == 2000


# ── LLM classification JSON in code block ──────────────────────────────────


@pytest.mark.asyncio
async def test_classify_skills_llm_json_in_text():
    """LLM wrapping JSON in extra text should still be parsed."""
    provider = MockProvider(responses=[
        'Here are the classifications: {"skill_a": "web", "skill_b": "math"}'
    ])
    skills = [
        SkillInfo(name="skill_a", description="fetch URL"),
        SkillInfo(name="skill_b", description="calculate"),
    ]
    result = await classify_skills_llm(skills, provider)
    assert result == {"skill_a": "web", "skill_b": "math"}
