"""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,
    QuoteRequest,
    QuoteSelection,
)
from app.models.decisions import LEDGER_TABLES
from app.models.places import Place, ShopPlace

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__,
    *LEDGER_TABLES, Place.__table__, ShopPlace.__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),
                              rename=kw.get("rename", False), check=False)


def test_alembic_has_one_head_and_the_chain_is_exactly_this():
    """Three lanes each chained a migration off #4 in the same week (two even chose the same id).
    The chain is serialised at integration: threads, then lines, then hours, then the panel's
    read state (R2b), then the quantity model, then the K1 renames, then the K2 ledger. One head, in this order."""
    script = ScriptDirectory.from_config(Config(str(MAIN / "alembic.ini")))
    assert script.get_heads() == ["c2d3e4f5a6b7"], script.get_heads()
    chain = {"c2d3e4f5a6b7": "e3f4a5b6c7d8", "e3f4a5b6c7d8": "d2e3f4a5b6c7", "d2e3f4a5b6c7": "c1d2e3f4a5b6", "c1d2e3f4a5b6": "b3c4d5e6f7a8", "b3c4d5e6f7a8": "a2b3c4d5e6f7", "a2b3c4d5e6f7": "f1a2b3c4d5e6", "f1a2b3c4d5e6": "f0a1b2c3d4e5", "f0a1b2c3d4e5": "e9f0a1b2c3d4", "e9f0a1b2c3d4": "d8e9f0a1b2c3", "d8e9f0a1b2c3": "c6d7e8f9a0b1", "c6d7e8f9a0b1": "c7d8e9f0a1b2",
             "c7d8e9f0a1b2": "b5c6d7e8f9a0", "b5c6d7e8f9a0": "a4b5c6d7e8f9"}
    for rev, down in chain.items():
        assert script.get_revision(rev).down_revision == down, rev


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)"


def test_authors_backfill_rename_rewrites_only_the_mapped_spellings_once(db):
    """`--rename` (rian, 14 Sep): the comments he moved in from Mark's and Adam's emails carried
    a typed author of "Mark (by email, 7 Sep)", which the page showed as the name. The one
    deliberate overwrite of a typed value: the mapped spelling becomes the account's display
    name on the rows it links, and nothing else moves. A second run changes zero rows."""
    cli.backfill_accounts(db)
    db.add(Account(display_name="Mark", username="mark", email="mark@example.com"))
    db.add_all([
        DiscussionComment(feature_key="structure:general", author="Mark (by email, 7 Sep)", body="from the email"),
        DiscussionComment(feature_key="structure:general", author="Mark (by email, 7 Sep)", body="also from the email"),
    ])
    db.commit()
    without = cli.backfill_authors(db, args(map=["Mark (by email, 7 Sep)=mark"]))
    assert "discussion_comments=2" in without and "renamed" not in without
    assert {c.author for c in db.scalars(select(DiscussionComment)) if c.body.endswith("the email")} == {"Mark (by email, 7 Sep)"}
    renamed = cli.backfill_authors(db, args(map=["Mark (by email, 7 Sep)=mark"], rename=True))
    assert "discussion_comments=0 renamed=2" in renamed
    db.expire_all()
    rows = {c.body: (c.author, c.author_id) for c in db.scalars(select(DiscussionComment))}
    assert rows["from the email"] == ("Mark", 3) and rows["also from the email"] == ("Mark", 3)
    assert rows["typed by Mark"] == ("Mark", None)  # the plain spelling was never mapped: untouched
    again = cli.backfill_authors(db, args(map=["Mark (by email, 7 Sep)=mark"], rename=True))
    assert "discussion_comments=0 renamed=0" in again
