"""Tests for lib/contributor.py — contributor attribution functions."""

# ruff: noqa: E402,I001

import asyncio
import os
import sqlite3
import sys
from unittest.mock import AsyncMock, MagicMock, patch

sys.modules.setdefault("aiohttp", MagicMock())

sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

from lib.contributor import (
    is_knowledge_pr,
    refine_commit_type,
    record_contributor_attribution,
    upsert_contributor,
    recalculate_tier,
)


# --- is_knowledge_pr ---

def test_knowledge_pr_domains():
    diff = "+++ b/domains/crypto/some-claim.md\n"
    assert is_knowledge_pr(diff) is True

def test_knowledge_pr_core():
    diff = "+++ b/core/epistemology.md\n"
    assert is_knowledge_pr(diff) is True

def test_knowledge_pr_foundations():
    diff = "--- a/foundations/overview.md\n"
    assert is_knowledge_pr(diff) is True

def test_knowledge_pr_decisions():
    diff = "+++ b/decisions/some-decision.md\n"
    assert is_knowledge_pr(diff) is True

def test_pipeline_only_pr():
    diff = "+++ b/inbox/source.md\n+++ b/entities/metadao.md\n"
    assert is_knowledge_pr(diff) is False

def test_mixed_pr_counts_as_knowledge():
    diff = "+++ b/inbox/source.md\n+++ b/domains/crypto/claim.md\n"
    assert is_knowledge_pr(diff) is True

def test_empty_diff():
    assert is_knowledge_pr("") is False


# --- refine_commit_type ---

def test_refine_non_extract_unchanged():
    assert refine_commit_type("anything", "research") == "research"
    assert refine_commit_type("anything", "entity") == "entity"

def test_refine_extract_new_files():
    diff = "diff --git a/x b/y\nnew file\n+++ b/domains/crypto/claim.md\n"
    assert refine_commit_type(diff, "extract") == "extract"

def test_refine_extract_challenge():
    diff = "diff --git a/x b/y\n+++ b/domains/crypto/claim.md\n+challenged_by: other\n"
    assert refine_commit_type(diff, "extract") == "challenge"

def test_refine_extract_enrich():
    diff = "diff --git a/x b/y\n+++ b/domains/crypto/claim.md\n+confidence: 0.8\n"
    assert refine_commit_type(diff, "extract") == "enrich"

def test_refine_extract_mixed_new_and_modified():
    diff = (
        "diff --git a/x b/y\nnew file\n+++ b/domains/crypto/new.md\n"
        "diff --git a/x b/z\n+++ b/domains/crypto/existing.md\n+foo\n"
    )
    assert refine_commit_type(diff, "extract") == "extract"


# --- upsert_contributor + recalculate_tier ---

def _make_db():
    conn = sqlite3.connect(":memory:")
    conn.row_factory = sqlite3.Row
    conn.execute("""CREATE TABLE contributors (
        handle TEXT PRIMARY KEY,
        agent_id TEXT,
        tier TEXT DEFAULT 'new',
        first_contribution TEXT,
        last_contribution TEXT,
        claims_merged INTEGER DEFAULT 0,
        challenges_survived INTEGER DEFAULT 0,
        sourcer_count INTEGER DEFAULT 0,
        extractor_count INTEGER DEFAULT 0,
        challenger_count INTEGER DEFAULT 0,
        synthesizer_count INTEGER DEFAULT 0,
        reviewer_count INTEGER DEFAULT 0,
        updated_at TEXT
    )""")
    conn.execute("""CREATE TABLE audit_log (
        id INTEGER PRIMARY KEY,
        ts TEXT DEFAULT (datetime('now')),
        stage TEXT,
        event TEXT,
        detail TEXT
    )""")
    return conn

def test_upsert_new_contributor():
    conn = _make_db()
    with patch("lib.contributor.config") as mock_config:
        mock_config.CONTRIBUTOR_TIER_RULES = {
            "veteran": {"claims_merged": 50, "min_days_since_first": 90, "challenges_survived": 5},
            "contributor": {"claims_merged": 10},
        }
        upsert_contributor(conn, "rio", "uuid-123", "extractor", "2026-04-16")
    row = conn.execute("SELECT * FROM contributors WHERE handle = 'rio'").fetchone()
    assert row["extractor_count"] == 1
    assert row["claims_merged"] == 1
    assert row["tier"] == "new"

def test_upsert_increment():
    conn = _make_db()
    upsert_contributor(conn, "rio", "uuid-123", "extractor", "2026-04-16")
    upsert_contributor(conn, "rio", "uuid-123", "extractor", "2026-04-17")
    row = conn.execute("SELECT * FROM contributors WHERE handle = 'rio'").fetchone()
    assert row["extractor_count"] == 2
    assert row["claims_merged"] == 2

def test_upsert_reviewer_no_claim_increment():
    conn = _make_db()
    upsert_contributor(conn, "leo", None, "reviewer", "2026-04-16")
    row = conn.execute("SELECT * FROM contributors WHERE handle = 'leo'").fetchone()
    assert row["reviewer_count"] == 1
    assert row["claims_merged"] == 0

def test_upsert_unknown_role():
    conn = _make_db()
    upsert_contributor(conn, "rio", None, "wizard", "2026-04-16")
    row = conn.execute("SELECT * FROM contributors WHERE handle = 'rio'").fetchone()
    assert row is None  # Should not insert

def test_recalculate_tier_contributor():
    conn = _make_db()
    conn.execute(
        """INSERT INTO contributors (handle, claims_merged, challenges_survived, first_contribution, tier)
        VALUES ('rio', 15, 0, '2026-01-01', 'new')"""
    )
    with patch("lib.contributor.config") as mock_config:
        mock_config.CONTRIBUTOR_TIER_RULES = {
            "veteran": {"claims_merged": 50, "min_days_since_first": 90, "challenges_survived": 5},
            "contributor": {"claims_merged": 10},
        }
        recalculate_tier(conn, "rio")
    row = conn.execute("SELECT tier FROM contributors WHERE handle = 'rio'").fetchone()
    assert row["tier"] == "contributor"

def test_recalculate_tier_veteran():
    conn = _make_db()
    conn.execute(
        """INSERT INTO contributors (handle, claims_merged, challenges_survived, first_contribution, tier)
        VALUES ('rio', 60, 10, '2025-01-01', 'contributor')"""
    )
    with patch("lib.contributor.config") as mock_config:
        mock_config.CONTRIBUTOR_TIER_RULES = {
            "veteran": {"claims_merged": 50, "min_days_since_first": 90, "challenges_survived": 5},
            "contributor": {"claims_merged": 10},
        }
        recalculate_tier(conn, "rio")
    row = conn.execute("SELECT tier FROM contributors WHERE handle = 'rio'").fetchone()
    assert row["tier"] == "veteran"


# --- record_contributor_attribution ---

def _make_attribution_db():
    conn = _make_db()
    conn.execute("""CREATE TABLE prs (
        number INTEGER PRIMARY KEY,
        commit_type TEXT,
        agent TEXT,
        submitted_by TEXT,
        domain TEXT,
        source_channel TEXT,
        leo_verdict TEXT,
        domain_verdict TEXT,
        domain_agent TEXT,
        merged_at TEXT
    )""")
    conn.execute("INSERT INTO prs (number, commit_type, agent) VALUES (100, 'extract', 'rio')")
    return conn

def test_record_skips_pipeline_only():
    conn = _make_attribution_db()
    mock_diff = "+++ b/inbox/source.md\n"

    async def run():
        with patch("lib.contributor.get_pr_diff", new_callable=AsyncMock, return_value=mock_diff):
            git_fn = AsyncMock(return_value=(0, ""))
            await record_contributor_attribution(conn, 100, "extract/test", git_fn)

    asyncio.run(run())
    row = conn.execute("SELECT * FROM contributors").fetchone()
    assert row is None  # No attribution for pipeline-only

def test_record_fallback_to_pr_agent():
    conn = _make_attribution_db()
    mock_diff = "diff --git a/x b/domains/crypto/claim.md\nnew file\n+++ b/domains/crypto/claim.md\n+some content\n"

    async def run():
        with patch("lib.contributor.get_pr_diff", new_callable=AsyncMock, return_value=mock_diff):
            # First call: trailer log (no trailers), Second call: author log (bot name → skipped)
            git_fn = AsyncMock(
                side_effect=[
                    (0, "no trailers here"),
                    (0, "domains/crypto/claim.md"),
                    (0, ""),
                    (0, "m3taversal"),
                ]
            )
            with patch("lib.contributor.config") as mock_config:
                mock_config.CONTRIBUTOR_TIER_RULES = {
                    "veteran": {"claims_merged": 50, "min_days_since_first": 90, "challenges_survived": 5},
                    "contributor": {"claims_merged": 10},
                }
                await record_contributor_attribution(conn, 100, "extract/test", git_fn)

    asyncio.run(run())
    row = conn.execute("SELECT * FROM contributors WHERE handle = 'rio'").fetchone()
    assert row is not None
    assert row["extractor_count"] == 1


def test_record_fallback_to_git_author():
    """External contributors get credited via git commit author."""
    conn = _make_attribution_db()
    conn.execute("INSERT INTO prs (number, commit_type, agent) VALUES (200, 'contrib', 'external')")
    mock_diff = (
        "diff --git a/x b/domains/ai-alignment/claim.md\nnew file\n"
        "+++ b/domains/ai-alignment/claim.md\n+new content\n"
    )

    async def run():
        with patch("lib.contributor.get_pr_diff", new_callable=AsyncMock, return_value=mock_diff):
            # First call: trailer log (no trailers), Second call: author log (external name)
            git_fn = AsyncMock(
                side_effect=[
                    (0, "no trailers"),
                    (0, "domains/ai-alignment/claim.md"),
                    (0, ""),
                    (0, "Cameron-S1"),
                ]
            )
            with patch("lib.contributor.config") as mock_config:
                mock_config.CONTRIBUTOR_TIER_RULES = {
                    "veteran": {"claims_merged": 50, "min_days_since_first": 90, "challenges_survived": 5},
                    "contributor": {"claims_merged": 10},
                }
                await record_contributor_attribution(conn, 200, "contrib/cameron/challenge", git_fn)

    asyncio.run(run())
    row = conn.execute("SELECT * FROM contributors WHERE handle = 'cameron-s1'").fetchone()
    assert row is not None
    assert row["extractor_count"] == 1

def test_record_parses_pentagon_trailer():
    conn = _make_attribution_db()
    mock_diff = "+++ b/domains/crypto/claim.md\n+new file content\n"
    trailer = "Pentagon-Agent: Theseus <uuid-456>"

    async def run():
        with patch("lib.contributor.get_pr_diff", new_callable=AsyncMock, return_value=mock_diff):
            git_fn = AsyncMock(return_value=(0, trailer))
            with patch("lib.contributor.config") as mock_config:
                mock_config.CONTRIBUTOR_TIER_RULES = {
                    "veteran": {"claims_merged": 50, "min_days_since_first": 90, "challenges_survived": 5},
                    "contributor": {"claims_merged": 10},
                }
                await record_contributor_attribution(conn, 100, "extract/test", git_fn)

    asyncio.run(run())
    row = conn.execute("SELECT * FROM contributors WHERE handle = 'theseus'").fetchone()
    assert row is not None
    assert row["agent_id"] == "uuid-456"

def test_record_refines_commit_type():
    conn = _make_attribution_db()
    mock_diff = "diff --git a/x b/y\n+++ b/domains/crypto/claim.md\n+challenged_by: foo\n"

    async def run():
        with patch("lib.contributor.get_pr_diff", new_callable=AsyncMock, return_value=mock_diff):
            git_fn = AsyncMock(return_value=(0, ""))
            with patch("lib.contributor.config") as mock_config:
                mock_config.CONTRIBUTOR_TIER_RULES = {
                    "veteran": {"claims_merged": 50, "min_days_since_first": 90, "challenges_survived": 5},
                    "contributor": {"claims_merged": 10},
                }
                await record_contributor_attribution(conn, 100, "extract/test", git_fn)

    asyncio.run(run())
    row = conn.execute("SELECT commit_type FROM prs WHERE number = 100").fetchone()
    assert row["commit_type"] == "challenge"

def test_record_no_diff_returns_early():
    conn = _make_attribution_db()

    async def run():
        with patch("lib.contributor.get_pr_diff", new_callable=AsyncMock, return_value=None):
            git_fn = AsyncMock()
            await record_contributor_attribution(conn, 100, "extract/test", git_fn)
            git_fn.assert_not_called()

    asyncio.run(run())
