"""Tests for hub (attempts, notes, skills)."""

import tempfile
from pathlib import Path

from coral.hub.attempts import (
    archive_attempts,
    format_leaderboard,
    format_status_summary,
    get_agent_attempts,
    get_leaderboard,
    per_agent_class_counts,
    read_attempt,
    read_attempts,
    search_attempts,
    write_attempt,
)
from coral.hub.notes import (
    format_notes_list,
    get_recent_notes,
    list_notes,
    notes_by,
    notes_unattributed,
    read_note,
    search_notes,
)
from coral.hub.skills import get_skill_tree, list_skills, read_skill
from coral.types import Attempt


def _make_attempt(
    commit: str,
    agent: str = "agent-1",
    score: float = 0.5,
    title: str = "test",
    timestamp: str = "2026-03-11T10:00:00Z",
) -> Attempt:
    return Attempt(
        commit_hash=commit,
        agent_id=agent,
        title=title,
        score=score,
        status="improved",
        parent_hash=None,
        timestamp=timestamp,
    )


def test_status_summary_records_latest():
    with tempfile.TemporaryDirectory() as d:
        write_attempt(
            d, _make_attempt("aaa", score=0.5, title="first", timestamp="2026-03-11T10:00:00")
        )
        write_attempt(
            d, _make_attempt("bbb", score=0.9, title="best one", timestamp="2026-03-11T11:00:00")
        )
        write_attempt(
            d, _make_attempt("ccc", score=0.3, title="latest one", timestamp="2026-03-11T12:00:00")
        )

        summary = format_status_summary(d)
        assert "Best:  0.9000000000  (best one)" in summary
        # Latest is the most recent by time, distinct from best -> shown on its own line.
        assert "Latest: 0.3000000000  (latest one)" in summary


def test_status_summary_hides_latest_when_it_is_best():
    with tempfile.TemporaryDirectory() as d:
        write_attempt(
            d, _make_attempt("aaa", score=0.5, title="first", timestamp="2026-03-11T10:00:00")
        )
        write_attempt(
            d, _make_attempt("bbb", score=0.9, title="best latest", timestamp="2026-03-11T11:00:00")
        )

        summary = format_status_summary(d)
        # Newest attempt is also the best -> no redundant "Latest: <score>" line.
        # (The unrelated "First attempt | Latest: <time>" line is expected, so match
        # on the score-bearing prefix, not the bare word "Latest:".)
        assert not any(line.startswith("Latest: ") for line in summary.splitlines())


def test_attempts_crud():
    with tempfile.TemporaryDirectory() as d:
        a1 = _make_attempt("aaa111", score=0.8, title="approach A")
        a2 = _make_attempt("bbb222", agent="agent-2", score=0.6, title="approach B")

        write_attempt(d, a1)
        write_attempt(d, a2)

        all_attempts = read_attempts(d)
        assert len(all_attempts) == 2


def test_archive_attempts_soft_deletes_from_all_views():
    with tempfile.TemporaryDirectory() as d:
        write_attempt(d, _make_attempt("aaa111", score=0.5, title="kept"))
        write_attempt(d, _make_attempt("bbb222", score=0.9, title="discarded"))

        archived = archive_attempts(d, {"bbb222"}, reason="discarded by resume --from aaa111")

        assert archived == ["bbb222"]
        assert [a.commit_hash for a in read_attempts(d)] == ["aaa111"]
        assert [a.commit_hash for a in get_leaderboard(d)] == ["aaa111"]
        assert [a.commit_hash for a in get_agent_attempts(d, "agent-1")] == ["aaa111"]
        assert search_attempts(d, "discarded") == []
        assert "bbb222" not in format_status_summary(d)
        # The record survives on disk and stays resolvable by explicit hash.
        kept = read_attempt(d, "bbb222")
        assert kept is not None
        assert kept.archived
        assert kept.metadata["archive_reason"] == "discarded by resume --from aaa111"


def test_archive_attempts_ignores_unknown_and_empty():
    with tempfile.TemporaryDirectory() as d:
        write_attempt(d, _make_attempt("aaa111"))

        assert archive_attempts(d, set()) == []
        assert archive_attempts(d, {"nope"}) == []
        assert [a.commit_hash for a in read_attempts(d)] == ["aaa111"]


def test_leaderboard():
    with tempfile.TemporaryDirectory() as d:
        write_attempt(d, _make_attempt("a", score=0.3))
        write_attempt(d, _make_attempt("b", score=0.9))
        write_attempt(d, _make_attempt("c", score=0.6))

        top = get_leaderboard(d, top_n=2)
        assert len(top) == 2
        assert top[0].score == 0.9
        assert top[1].score == 0.6


def test_agent_filter():
    with tempfile.TemporaryDirectory() as d:
        write_attempt(d, _make_attempt("a", agent="agent-1"))
        write_attempt(d, _make_attempt("b", agent="agent-2"))
        write_attempt(d, _make_attempt("c", agent="agent-1"))

        agent1 = get_agent_attempts(d, "agent-1")
        assert len(agent1) == 2


def test_agent_filter_scans_migrated_agent_current_island():
    """A prefixed agent id is birth lineage, not current island after migration."""
    with tempfile.TemporaryDirectory() as d:
        coral_dir = Path(d)
        for island in ("0", "1"):
            (coral_dir / "islands" / island / "attempts").mkdir(parents=True)

        write_attempt(coral_dir, _make_attempt("after-move", agent="0-agent-1"), island_id="1")

        assert get_agent_attempts(coral_dir, "0-agent-1", island_id="0") == []
        assert len(get_agent_attempts(coral_dir, "0-agent-1", island_id="1")) == 1
        assert len(get_agent_attempts(coral_dir, "0-agent-1")) == 1


def test_agent_filter_scans_string_named_islands():
    """island_id=None must scan name-based islands, not only numeric ones.

    Regression: the run-wide scan filtered view roots with
    ``r.name.isdigit()``, returning nothing for named islands (the
    ``coral start`` default: ``atlantis``, ``avalon``, ...).
    """
    with tempfile.TemporaryDirectory() as d:
        coral_dir = Path(d)
        for island in ("atlantis", "avalon"):
            (coral_dir / "islands" / island / "attempts").mkdir(parents=True)

        write_attempt(
            coral_dir, _make_attempt("h1", agent="ahab-from-atlantis"), island_id="atlantis"
        )
        write_attempt(
            coral_dir, _make_attempt("h2", agent="sparrow-from-avalon"), island_id="avalon"
        )

        assert [a.commit_hash for a in get_agent_attempts(coral_dir, "ahab-from-atlantis")] == [
            "h1"
        ]
        assert [a.commit_hash for a in get_agent_attempts(coral_dir, "sparrow-from-avalon")] == [
            "h2"
        ]


def test_search():
    with tempfile.TemporaryDirectory() as d:
        write_attempt(d, _make_attempt("a", title="learning rate tuning"))
        write_attempt(d, _make_attempt("b", title="attention heads"))
        write_attempt(d, _make_attempt("c", title="learning rate schedule"))

        results = search_attempts(d, "learning rate")
        assert len(results) == 2


def test_format_leaderboard():
    attempts = [_make_attempt("a", score=0.9), _make_attempt("b", score=0.5)]
    md = format_leaderboard(attempts)
    assert "Rank" in md
    assert "0.9000" in md


def test_format_leaderboard_shows_class_column():
    """The Class column distinguishes real / tune / error attempts at a glance."""
    real = _make_attempt("aaa", score=0.9, title="real-row")
    tune = _make_attempt("bbb", score=0.5, title="tune-row")
    tune.metadata["budget_class"] = "tune"
    err = _make_attempt("ccc", score=0.3, title="error-row")
    err.metadata["budget_class"] = "grader_error"

    md = format_leaderboard([real, tune, err])
    assert "Class" in md
    # Per-row class labels appear in the table body.
    real_line = next(line for line in md.splitlines() if "real-row" in line)
    tune_line = next(line for line in md.splitlines() if "tune-row" in line)
    err_line = next(line for line in md.splitlines() if "error-row" in line)
    assert " real " in real_line
    assert " tune " in tune_line
    # grader_error is rendered as compact "error" to keep the column narrow.
    assert " error " in err_line
    assert "grader_error" not in err_line


def test_per_agent_class_counts_splits_by_budget_class():
    """Budget class counts are tallied per agent (issue #73)."""
    with tempfile.TemporaryDirectory() as d:
        # agent-1: 2 real, 1 grader_error, 1 tune
        a = _make_attempt("aaa", agent="agent-1")
        b = _make_attempt("bbb", agent="agent-1")
        c = _make_attempt("ccc", agent="agent-1")
        c.metadata["budget_class"] = "grader_error"
        c.status = "timeout"
        d_att = _make_attempt("ddd", agent="agent-1")
        d_att.metadata["budget_class"] = "tune"

        # agent-2: 1 real
        e = _make_attempt("eee", agent="agent-2")

        for att in (a, b, c, d_att, e):
            write_attempt(d, att)

        counts = per_agent_class_counts(d)
        assert counts["agent-1"] == {"real": 2, "grader_error": 1, "tune": 1}
        assert counts["agent-2"] == {"real": 1}


def test_per_agent_class_counts_skips_pending():
    """Pending attempts have no final classification — exclude from tallies."""
    with tempfile.TemporaryDirectory() as d:
        scored = _make_attempt("aaa", agent="agent-1")
        pending = _make_attempt("bbb", agent="agent-1")
        pending.status = "pending"
        pending.score = None

        write_attempt(d, scored)
        write_attempt(d, pending)

        counts = per_agent_class_counts(d)
        assert counts["agent-1"] == {"real": 1}


def test_notes():
    with tempfile.TemporaryDirectory() as d:
        # Write notes in public/notes/notes.md
        (Path(d) / "public" / "notes").mkdir(parents=True)
        notes_file = Path(d) / "public" / "notes" / "notes.md"
        notes_file.write_text(
            "## [2026-03-11] ReLU works better\n"
            "Details about ReLU activation...\n"
            "\n"
            "## [2026-03-11] Learning rate 0.001 is optimal\n"
            "Tried various learning rates...\n"
        )

        entries = list_notes(d)
        assert len(entries) == 2
        assert entries[0]["title"] == "ReLU works better"
        assert entries[1]["title"] == "Learning rate 0.001 is optimal"

        # Read specific entry
        content = read_note(d, 1)
        assert content is not None
        assert "ReLU" in content
        assert "Details" in content

        # Search
        results = search_notes(d, "learning rate")
        assert len(results) == 1
        assert results[0]["title"] == "Learning rate 0.001 is optimal"

        # Recent
        recent = get_recent_notes(d, n=1)
        assert len(recent) == 1
        assert recent[0]["title"] == "Learning rate 0.001 is optimal"

        # Format
        formatted = format_notes_list(entries)
        assert "ReLU" in formatted
        assert "Learning rate" in formatted


def test_notes_empty():
    with tempfile.TemporaryDirectory() as d:
        entries = list_notes(d)
        assert entries == []
        assert format_notes_list(entries) == "No notes yet."


def test_notes_skip_index_and_raw_sources():
    with tempfile.TemporaryDirectory() as d:
        notes_dir = Path(d) / "public" / "notes"
        raw_dir = notes_dir / "raw"
        research_dir = notes_dir / "research"
        synthesis_dir = notes_dir / "_synthesis"
        raw_dir.mkdir(parents=True)
        research_dir.mkdir()
        synthesis_dir.mkdir()
        (notes_dir / "index.md").write_text(
            "# Notes Index\n\n- [Useful idea](research/useful-idea.md)\n"
        )
        (raw_dir / "paper.md").write_text(
            "---\ncreator: raw-agent\n---\n\n# Raw paper\n\nneedle-only-in-raw\n"
        )
        (raw_dir / "unstamped.md").write_text("# Raw source without frontmatter\n")
        (research_dir / "useful-idea.md").write_text(
            "---\ncreator: agent-1\n---\n\n# Useful idea\n"
        )
        (synthesis_dir / "team-roster.md").write_text(
            "---\ncreator: synthesizer\n---\n\n# Team roster\n"
        )

        entries = list_notes(d)
        assert {e["relative_path"] for e in entries} == {
            str(Path("_synthesis") / "team-roster.md"),
            str(Path("research") / "useful-idea.md"),
        }
        assert search_notes(d, "needle-only-in-raw") == []
        assert [e["relative_path"] for e in search_notes(d, "roster")] == [
            str(Path("_synthesis") / "team-roster.md")
        ]
        assert notes_by(d, None, "raw-agent") == []
        assert [p.relative_to(notes_dir) for p in notes_by(d, None, "synthesizer")] == [
            Path("_synthesis") / "team-roster.md"
        ]


def test_list_notes_include_raw_surfaces_sources_only_when_asked():
    """include_raw=True adds raw/ captures (category 'raw') for the dashboard,
    while the default keeps agent-facing callers seeing only authored notes."""
    with tempfile.TemporaryDirectory() as d:
        notes_dir = Path(d) / "public" / "notes"
        raw_dir = notes_dir / "raw"
        research_dir = notes_dir / "research"
        raw_dir.mkdir(parents=True)
        research_dir.mkdir()
        (research_dir / "useful-idea.md").write_text(
            "---\ncreator: agent-1\n---\n\n# Useful idea\n"
        )
        (raw_dir / "paper.md").write_text(
            "---\nsource_url: http://x\nsource_type: paper\ncaptured: 2026-01-01\n---\n\n"
            "# Wang integer hash\n\nneedle-only-in-raw\n"
        )
        # `_`-prefixed meta under raw/ is never a source; must stay excluded.
        (raw_dir / "_scratch.md").write_text("# scratch\n")

        # Default: raw is still excluded (CLI / search / heartbeat unchanged).
        default = list_notes(d)
        assert {e["relative_path"] for e in default} == {str(Path("research") / "useful-idea.md")}

        # Opt-in: the raw source shows up, tagged category "raw"; the note stays.
        with_raw = list_notes(d, include_raw=True)
        by_path = {e["relative_path"]: e for e in with_raw}
        assert set(by_path) == {
            str(Path("research") / "useful-idea.md"),
            str(Path("raw") / "paper.md"),
        }
        raw_entry = by_path[str(Path("raw") / "paper.md")]
        assert raw_entry["category"] == "raw"
        assert raw_entry["title"] == "Wang integer hash"
        assert list(by_path).count(str(Path("raw") / "_scratch.md")) == 0


def test_raw_source_surfaces_provenance_frontmatter():
    """Raw sources use a source vocabulary (source_url / captured / retrieved_by).
    Those must reach the dashboard — dropping source_url hides where a cited
    source came from, which is the whole point of a source."""
    with tempfile.TemporaryDirectory() as d:
        raw_dir = Path(d) / "public" / "notes" / "raw"
        raw_dir.mkdir(parents=True)
        # Title only in frontmatter (no `# heading`); source vocabulary throughout.
        (raw_dir / "jenkins.md").write_text(
            "---\n"
            "source_url: https://pastebin.com/raw/5ucHpK7v\n"
            "source_type: code\n"
            "captured: 2026-07-24\n"
            "retrieved_by: captain-nemo\n"
            "title: Jenkins 32-bit integer hash\n"
            "also_confirmed_by:\n"
            "  - https://en.wikipedia.org/wiki/Jenkins_hash_function\n"
            "  - https://gist.github.com/lh3/59882d6b96166dfc3d8d\n"
            "---\n\nbody text\n"
        )

        entry = next(e for e in list_notes(d, include_raw=True) if e["category"] == "raw")
        assert entry["source_url"] == "https://pastebin.com/raw/5ucHpK7v"
        assert entry["source_type"] == "code"
        assert entry["date"] == "2026-07-24"  # captured -> date
        assert entry["creator"] == "captain-nemo"  # retrieved_by -> creator
        assert entry["title"] == "Jenkins 32-bit integer hash"  # frontmatter title
        assert entry["also_confirmed_by"] == [
            "https://en.wikipedia.org/wiki/Jenkins_hash_function",
            "https://gist.github.com/lh3/59882d6b96166dfc3d8d",
        ]
        # Complete frontmatter passthrough — every authored field, so the
        # dashboard can show all of it rather than a curated subset.
        assert entry["frontmatter"] == {
            "source_url": "https://pastebin.com/raw/5ucHpK7v",
            "source_type": "code",
            "captured": "2026-07-24",
            "retrieved_by": "captain-nemo",
            "title": "Jenkins 32-bit integer hash",
            "also_confirmed_by": [
                "https://en.wikipedia.org/wiki/Jenkins_hash_function",
                "https://gist.github.com/lh3/59882d6b96166dfc3d8d",
            ],
        }


def test_research_note_surfaces_full_frontmatter():
    """A research note must expose all of its schema fields (creator/created/
    type/confidence/based_on/tags), not just the curated trace subset."""
    with tempfile.TemporaryDirectory() as d:
        research_dir = Path(d) / "public" / "notes" / "research"
        research_dir.mkdir(parents=True)
        (research_dir / "algo.md").write_text(
            "---\n"
            "creator: captain-ahab\n"
            "created: 2026-07-24\n"
            "type: research\n"
            "confidence: high\n"
            "tags: [algorithm, tree-traversal]\n"
            "based_on:\n"
            "  - raw/frozen-machine-isa.md\n"
            "---\n\n# Algorithm structure\n\nbody\n"
        )
        entry = next(e for e in list_notes(d) if e["category"] == "research")
        assert entry["frontmatter"] == {
            "creator": "captain-ahab",
            "created": "2026-07-24",
            "type": "research",
            "confidence": "high",
            "tags": ["algorithm", "tree-traversal"],
            "based_on": ["raw/frozen-machine-isa.md"],
        }
        assert notes_unattributed(d, None) == []


def test_skills():
    with tempfile.TemporaryDirectory() as d:
        skill_dir = Path(d) / "public" / "skills" / "my_tool"
        skill_dir.mkdir(parents=True)
        scripts_dir = skill_dir / "scripts"
        scripts_dir.mkdir()

        (skill_dir / "SKILL.md").write_text(
            "---\nname: my_tool\ndescription: A useful tool\ncreator: agent-1\n---\n# My Tool\nUsage..."
        )
        (scripts_dir / "run.py").write_text("print('hello')")

        skills = list_skills(d)
        assert len(skills) == 1
        assert skills[0]["name"] == "my_tool"

        info = read_skill(str(skill_dir))
        assert "run.py" in str(info["files"])
        assert "Usage" in info["body"]

        tree = get_skill_tree(str(skill_dir))
        assert "SKILL.md" in tree
