"""
BIBIVIDI — Marketplace payout tests (TODO-22)
Tests for bvd/marketplace/payouts.py: compute_author_split, record/list/mark payouts.
"""
from __future__ import annotations

import pytest

from bvd.marketplace.payouts import (
    CHAIN_SHARE,
    DEP_POOL,
    AuthorSplit,
    PayoutRow,
    compute_author_split,
    list_pending_payouts,
    mark_payout_paid,
    record_execution_payouts,
)

# ── helpers ────────────────────────────────────────────────────────────────────

class _FakeScript:
    def __init__(
        self,
        sid: str,
        price: float | None = None,
        author_id: str | None = None,
        chain_ids: list[str] | None = None,
        step_weights: dict[str, float] | None = None,
    ):
        self.id = sid
        self.marketplace_price_eur = price
        self.author_id = author_id
        self.chain_ids = chain_ids or []
        self.step_weights = step_weights

    @property
    def is_paid(self) -> bool:
        return self.marketplace_price_eur is not None and self.marketplace_price_eur > 0


def _chain(price: float = 1.0, author_id: str = "acct_chain",
           chain_ids: list[str] | None = None,
           step_weights: dict[str, float] | None = None) -> _FakeScript:
    return _FakeScript("chain-skill", price, author_id,
                       chain_ids or ["dep-a", "dep-b"], step_weights)


def _dep(sid: str, author_id: str | None = None) -> _FakeScript:
    return _FakeScript(sid, author_id=author_id)


# ── compute_author_split ───────────────────────────────────────────────────────

def test_free_skill_returns_empty():
    chain = _FakeScript("free-chain", price=None)
    split = compute_author_split(chain, [])
    assert split.gross_revenue_eur == 0.0
    assert split.rows == []


def test_zero_price_returns_empty():
    chain = _FakeScript("zero-chain", price=0.0)
    split = compute_author_split(chain, [])
    assert split.gross_revenue_eur == 0.0
    assert split.rows == []


def test_chain_author_row_created():
    chain = _chain(price=2.0, author_id="acct_chain", chain_ids=[])
    split = compute_author_split(chain, [])
    assert len(split.rows) == 1
    row = split.rows[0]
    assert row.role == "chain"
    assert row.skill_id == "chain-skill"
    assert row.author_id == "acct_chain"
    assert row.weight == CHAIN_SHARE
    assert abs(row.author_share_eur - 2.0 * CHAIN_SHARE) < 1e-6


def test_dep_rows_created():
    deps = [_dep("dep-a", "acct_a"), _dep("dep-b", "acct_b")]
    split = compute_author_split(_chain(price=1.0), deps)
    dep_rows = [r for r in split.rows if r.role == "dep"]
    assert len(dep_rows) == 2
    dep_ids = {r.skill_id for r in dep_rows}
    assert dep_ids == {"dep-a", "dep-b"}


def test_dep_equal_weight():
    deps = [_dep("dep-a", "acct_a"), _dep("dep-b", "acct_b")]
    split = compute_author_split(_chain(price=1.0), deps)
    dep_rows = [r for r in split.rows if r.role == "dep"]
    for r in dep_rows:
        assert abs(r.weight - DEP_POOL / 2) < 1e-6


def test_custom_step_weights_applied():
    weights = {"dep-a": 0.30, "dep-b": 0.10}
    deps = [_dep("dep-a", "acct_a"), _dep("dep-b", "acct_b")]
    split = compute_author_split(_chain(price=1.0, step_weights=weights), deps)
    dep_rows = {r.skill_id: r for r in split.rows if r.role == "dep"}
    assert abs(dep_rows["dep-a"].weight - 0.30) < 1e-6
    assert abs(dep_rows["dep-b"].weight - 0.10) < 1e-6


def test_platform_retains_remainder():
    deps = [_dep("dep-a", "acct_a"), _dep("dep-b", "acct_b")]
    price = 1.0
    split = compute_author_split(_chain(price=price), deps)
    total_shares = sum(r.author_share_eur for r in split.rows)
    platform_share = price - total_shares
    # Platform keeps at least 20 %
    assert platform_share >= price * 0.20 - 1e-6


def test_gross_revenue_matches_price():
    split = compute_author_split(_chain(price=3.5), [])
    assert split.gross_revenue_eur == 3.5


def test_dep_with_no_author_included():
    """Deps without author_id still get a row (platform receives their share)."""
    deps = [_dep("dep-a", None)]
    split = compute_author_split(_chain(price=1.0, chain_ids=["dep-a"]), deps)
    dep_rows = [r for r in split.rows if r.role == "dep"]
    assert len(dep_rows) == 1
    assert dep_rows[0].author_id is None


def test_none_dep_entry_skipped():
    """None entries in dep_scripts list are skipped gracefully."""
    deps = [_dep("dep-a", "acct_a"), None]  # type: ignore[list-item]
    split = compute_author_split(_chain(price=1.0), deps)
    dep_rows = [r for r in split.rows if r.role == "dep"]
    assert all(r.skill_id != "dep-b" for r in dep_rows)


def test_step_num_assigned():
    deps = [_dep("dep-a", "acct_a"), _dep("dep-b", "acct_b")]
    split = compute_author_split(_chain(price=1.0), deps)
    dep_rows = sorted(
        [r for r in split.rows if r.role == "dep"], key=lambda r: r.step_num  # type: ignore
    )
    assert dep_rows[0].step_num == 0
    assert dep_rows[1].step_num == 1


def test_chain_row_step_num_is_none():
    split = compute_author_split(_chain(price=1.0, chain_ids=[]), [])
    chain_row = next(r for r in split.rows if r.role == "chain")
    assert chain_row.step_num is None


# ── record_execution_payouts / list_pending_payouts / mark_payout_paid ─────────

@pytest.mark.asyncio
async def test_record_and_list_payouts(tmp_path):
    from unittest.mock import patch

    db = tmp_path / "test.db"
    with patch("bvd.config.settings.db_path", db):
        from bvd.engine.library import Library
        lib = Library()
        await lib.init()

        # Insert a fake execution row to satisfy FK
        import aiosqlite
        async with aiosqlite.connect(db) as conn:
            await conn.execute(
                "INSERT INTO scripts (id, name, description, code, created_at, updated_at) "
                "VALUES ('s1', 'S1', 'desc', '', datetime('now'), datetime('now'))"
            )
            await conn.execute(
                "INSERT INTO executions (script_id, tokens_before, tokens_after, "
                "latency_ms, executed_at) VALUES ('s1', 100, 10, 5.0, datetime('now'))"
            )
            async with conn.execute("SELECT last_insert_rowid()") as cur:
                row = await cur.fetchone()
            execution_id = row[0]
            await conn.commit()

        split = AuthorSplit(
            gross_revenue_eur=1.0,
            rows=[
                PayoutRow("s1", "acct_chain", "chain", None, 0.40, 1.0, 0.40),
                PayoutRow("s1", "acct_dep", "dep", 0, 0.20, 1.0, 0.20),
            ],
        )
        with patch("bvd.marketplace.payouts.settings.db_path", db):
            await record_execution_payouts(execution_id, split)
            payouts = await list_pending_payouts()

        await lib.close()

    assert len(payouts) == 2
    roles = {p["role"] for p in payouts}
    assert roles == {"chain", "dep"}


@pytest.mark.asyncio
async def test_mark_payout_paid(tmp_path):
    from unittest.mock import patch

    db = tmp_path / "test.db"
    with patch("bvd.config.settings.db_path", db):
        from bvd.engine.library import Library
        lib = Library()
        await lib.init()

        import aiosqlite
        async with aiosqlite.connect(db) as conn:
            await conn.execute(
                "INSERT INTO scripts (id, name, description, code, created_at, updated_at) "
                "VALUES ('s2', 'S2', 'desc', '', datetime('now'), datetime('now'))"
            )
            await conn.execute(
                "INSERT INTO executions (script_id, tokens_before, tokens_after, "
                "latency_ms, executed_at) VALUES ('s2', 100, 10, 5.0, datetime('now'))"
            )
            async with conn.execute("SELECT last_insert_rowid()") as cur:
                row = await cur.fetchone()
            execution_id = row[0]
            await conn.commit()

        split = AuthorSplit(
            gross_revenue_eur=0.5,
            rows=[PayoutRow("s2", "acct_chain", "chain", None, 0.40, 0.5, 0.20)],
        )
        with patch("bvd.marketplace.payouts.settings.db_path", db):
            await record_execution_payouts(execution_id, split)
            payouts = await list_pending_payouts()
            payout_id = payouts[0]["id"]
            ok = await mark_payout_paid(payout_id)
            remaining = await list_pending_payouts()

        await lib.close()

    assert ok is True
    assert len(remaining) == 0


@pytest.mark.asyncio
async def test_mark_payout_paid_not_found(tmp_path):
    from unittest.mock import patch

    db = tmp_path / "test.db"
    with patch("bvd.config.settings.db_path", db):
        from bvd.engine.library import Library
        lib = Library()
        await lib.init()
        await lib.close()

        with patch("bvd.marketplace.payouts.settings.db_path", db):
            ok = await mark_payout_paid(9999)

    assert ok is False


@pytest.mark.asyncio
async def test_empty_split_records_nothing(tmp_path):
    from unittest.mock import patch

    db = tmp_path / "test.db"
    with patch("bvd.config.settings.db_path", db):
        from bvd.engine.library import Library
        lib = Library()
        await lib.init()
        await lib.close()

        split = AuthorSplit(gross_revenue_eur=0.0, rows=[])
        with patch("bvd.marketplace.payouts.settings.db_path", db):
            await record_execution_payouts(1, split)  # execution_id FK won't matter
            payouts = await list_pending_payouts()

    assert payouts == []


# ── stats_tracker.record returns execution_id ─────────────────────────────────

@pytest.mark.asyncio
async def test_stats_record_returns_execution_id(tmp_path):
    from unittest.mock import patch

    db = tmp_path / "test.db"
    with patch("bvd.config.settings.db_path", db):
        from bvd.engine.library import Library
        from bvd.engine.stats import StatsTracker

        lib = Library()
        await lib.init()

        import aiosqlite
        async with aiosqlite.connect(db) as conn:
            await conn.execute(
                "INSERT INTO scripts (id, name, description, code, created_at, updated_at) "
                "VALUES ('sk', 'Sk', 'desc', '', datetime('now'), datetime('now'))"
            )
            await conn.commit()

        tracker = StatsTracker()
        eid = await tracker.record("sk", 1000, 100, 10.0)
        await lib.close()

    assert isinstance(eid, int)
    assert eid >= 1


@pytest.mark.asyncio
async def test_stats_record_no_script_returns_none(tmp_path):
    from unittest.mock import patch

    db = tmp_path / "test.db"
    with patch("bvd.config.settings.db_path", db):
        from bvd.engine.library import Library
        from bvd.engine.stats import StatsTracker

        lib = Library()
        await lib.init()
        await lib.close()

        tracker = StatsTracker()
        result = await tracker.record(None, 1000, 100)

    assert result is None
