"""Migration #4's backfills are idempotent, and the migration chain has one head.

Rehearsed on a copy of the 10 Sep nightly dump: `backfill accounts` gave the seeded owner row
its username, `levels` created admin and member, `authors --map rian=rian` linked 34 comments
and 8 running-list states, and a second run of each changed zero rows. This pins that on an
in-memory SQLite so a later edit cannot make a backfill write on its second run. No network,
no Postgres: the tables the backfills touch are created here from the models.
"""

import argparse
import pathlib

import pytest
from alembic.config import Config
from alembic.script import ScriptDirectory
from sqlalchemy import create_engine, select
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool

from app import cli
from app.models import (
    Account,
    AccountLevel,
    Base,
    ClientTodo,
    ClientUpload,
    DiscussionComment,
    DiscussionItem,
    FeaturePriority,
    OwnerItemState,
    Override,
    QuoteRequest,
    QuoteSelection,
)

MAIN = pathlib.Path(__file__).resolve().parents[1]
TABLES = [
    Account.__table__, AccountLevel.__table__, DiscussionItem.__table__, DiscussionComment.__table__,
    FeaturePriority.__table__, QuoteSelection.__table__, QuoteRequest.__table__,
    ClientTodo.__table__, ClientUpload.__table__, OwnerItemState.__table__, Override.__table__,
]


@pytest.fixture
def db():
    engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
    Base.metadata.create_all(engine, tables=TABLES)
    session = sessionmaker(bind=engine, expire_on_commit=False)()
    # The seeded owner row as migration #1 left it: no username yet.
    session.add(Account(display_name="rian"))
    session.add(Account(display_name="Adam", username="adam", email="adam@example.com"))
    session.add_all([
        DiscussionComment(feature_key="structure:urls", author="Mark", body="typed by Mark"),
        DiscussionComment(feature_key="structure:urls", author=" adam ", body="typed with spaces"),
        DiscussionComment(feature_key="structure:urls", author="Rian", body="typed by rian"),
        QuoteRequest(author="Adam", items="[]", total_usd=1),
        OwnerItemState(item_id="x", status="done", acted_by="rian"),
        FeaturePriority(feature_key="f", priority="essential", author="Adam"),
        QuoteSelection(item_key="q", included=True, author="Adam"),
    ])
    session.commit()
    yield session
    session.close()


def args(**kw) -> argparse.Namespace:
    return argparse.Namespace(map=kw.get("map", []), include_defaulted=kw.get("include_defaulted", False), check=False)


def test_alembic_has_one_head_and_migration_4_is_in_the_chain():
    script = ScriptDirectory.from_config(Config(str(MAIN / "alembic.ini")))
    heads = script.get_heads()
    assert heads == ["c6d7e8f9a0b1"], heads  # migration #6 (Stream M) revises #4
    assert script.get_revision("c6d7e8f9a0b1").down_revision == "b5c6d7e8f9a0"
    assert script.get_revision("b5c6d7e8f9a0").down_revision == "a4b5c6d7e8f9"


def test_accounts_backfill_names_the_owner_row_once(db):
    assert cli.backfill_accounts(db).endswith("gained username")
    assert "already present" in cli.backfill_accounts(db)
    owner = db.scalar(select(Account).where(Account.display_name == "rian"))
    assert owner.username == "rian" and owner.status == "active"


def test_levels_backfill_inserts_only_the_absent(db):
    assert cli.backfill_levels(db).startswith("levels: created admin, member")
    # An edited level is never rewritten: the seed is append-only.
    db.get(AccountLevel, "member").permissions = ["client.view"]
    db.commit()
    assert cli.backfill_levels(db).startswith("levels: created none")
    assert db.get(AccountLevel, "member").permissions == ["client.view"]
    assert db.get(AccountLevel, "admin").permissions == ["client.view", "client.participate"]


def test_authors_backfill_links_by_folded_text_and_is_idempotent(db):
    cli.backfill_accounts(db)
    first = cli.backfill_authors(db, args(map=["Adam=adam", "rian=rian"]))
    assert "discussion_comments=2" in first and "quote_requests=1" in first and "owner_item_states=1" in first
    linked = {c.body: c.author_id for c in db.scalars(select(DiscussionComment))}
    assert linked["typed with spaces"] == 2 and linked["typed by rian"] == 1
    assert linked["typed by Mark"] is None  # no map for Mark: the text stays, the link stays empty
    second = cli.backfill_authors(db, args(map=["Adam=adam", "rian=rian"]))
    assert second == "authors: linked discussion_comments=0 quote_requests=0 client_todos=0 client_uploads=0 owner_item_states=0"


def test_authors_backfill_leaves_the_defaulted_tables_alone_unless_told(db):
    cli.backfill_authors(db, args(map=["Adam=adam"]))
    assert db.scalar(select(FeaturePriority)).author_id is None
    assert db.scalar(select(QuoteSelection)).author_id is None
    out = cli.backfill_authors(db, args(map=["Adam=adam"], include_defaulted=True))
    assert "feature_priorities=1" in out and "quote_selections=1" in out
    assert db.scalar(select(FeaturePriority)).author_id == 2


def test_authors_backfill_refuses_an_unknown_account(db):
    with pytest.raises(ValueError, match="no account with username 'nobody'"):
        cli.backfill_authors(db, args(map=["Adam=nobody"]))


def test_overrides_backfill_writes_nothing(db):
    assert cli.backfill_overrides(db) == "overrides: 0 changed (0 row(s) present)"
