"""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
from app.services import accounts

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, then the K12 precedent
    register, then the AW3 picture columns, then the AW2 featured pin, then the AW4 collector control
    plane, then the AW5 article sample, tags and hero credit columns. One head, in this order."""
    script = ScriptDirectory.from_config(Config(str(MAIN / "alembic.ini")))
    assert script.get_heads() == ["aw5d1e2f3a4b"], script.get_heads()
    chain = {"aw5d1e2f3a4b": "aw4a1b2c3d4e", "aw4a1b2c3d4e": "aw2c1d2e3f4a", "aw2c1d2e3f4a": "aw3b1c2d3e4f", "aw3b1c2d3e4f": "k12a1b2c3d4e", "k12a1b2c3d4e": "c2d3e4f5a6b7", "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 == list(accounts.SEED_LEVELS["admin"]["permissions"]), \
        "the seed is read from one place, so a permission joining it later cannot leave this pin behind"


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
