"""Real-Postgres round trip for the note fields #3182 restored.

The unit lane can prove a serialiser EMITS a key. It cannot prove a restore
puts the value back on the right row, and the note->note edges are exactly
where that distinction bites: `parent_id` and `arose_from_id` hold ids from
the SOURCE database, so a restore that writes them straight through succeeds,
reports success, and silently points every edge at whatever record happens to
hold that number here.

That is why #3182 was not fixed in passing.

These drive the REAL `restore_full_backup`, not a reimplementation of its
loop. A test that re-derives the remap it is checking would agree with
whatever the product does, including nothing.
"""
import pytest
import pytest_asyncio
from sqlalchemy import select

from scribe.models import async_session
from scribe.models.note import Note
from scribe.models.user import User
from scribe.services import backup
from tests.helpers import ensure_user

pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]

RESTORED_USERNAME = "backup_roundtrip_restored"


@pytest_asyncio.fixture
async def source():
    """A snippet, a process, and an issue that arose from a work task — the
    shapes whose identity the backup used to drop — exported as backup rows.

    Deleted children-first: `arose_from_id` is a real FK, so removing the
    origin while the issue still points at it is asking the database a
    question the test has no reason to ask.
    """
    async with async_session() as s:
        owner = await ensure_user(s, "backup_roundtrip_owner")
        uid = owner.id
        await s.commit()

    async with async_session() as s:
        origin = Note(
            user_id=uid, title="the work that broke", body="",
            status="done", task_kind="work",
        )
        snippet = Note(
            user_id=uid, title="debounce — rate-limit", body="```js\n1\n```",
            note_type="snippet", data={"name": "debounce", "language": "js"},
        )
        process = Note(
            user_id=uid, title="DRY pass", body="steps", note_type="process",
        )
        s.add_all([origin, snippet, process])
        await s.commit()
        origin_id = origin.id

        issue = Note(
            user_id=uid, title="the fix", body="symptom -> cause -> fix",
            status="done", task_kind="issue", arose_from_id=origin_id,
            description="one-liner",
        )
        s.add(issue)
        await s.commit()
        order = [issue.id, origin_id, snippet.id, process.id]

    async with async_session() as s:
        rows = (await s.execute(select(Note).where(Note.id.in_(order)))).scalars().all()
        note_rows = backup._note_rows(list(rows))
        user_rows = backup._user_rows(
            [(await s.execute(select(User).where(User.id == uid))).scalars().one()]
        )
    # Restore mints a NEW user from the payload, so the restored corpus is
    # entirely separate from the source — which is what makes the id
    # assertions meaningful. Renamed here rather than in a sibling fixture:
    # `restored` depends on this one, and a rename elsewhere might not have
    # run by the time the restore does.
    user_rows[0]["username"] = RESTORED_USERNAME

    yield {
        "payload": {
            "version": backup.BACKUP_VERSION,
            "users": user_rows,
            "notes": note_rows,
        },
        "origin_id": origin_id,
        "owner_id": uid,
    }

    async with async_session() as s:
        for nid in order:
            row = await s.get(Note, nid)
            if row is not None:
                await s.delete(row)
        await s.commit()


@pytest_asyncio.fixture
async def restored(source):
    """Run the real restore, then hand back the new rows by title.

    The restore mints a NEW user from the payload, so the restored corpus is
    entirely separate from the source one — which is what makes the id
    assertions below meaningful.
    """
    await backup.restore_full_backup(source["payload"])

    async with async_session() as s:
        user = (await s.execute(
            select(User).where(User.username == RESTORED_USERNAME)
        )).scalars().first()
        assert user is not None, "the payload's user was not restored"
        rows = (await s.execute(
            select(Note).where(Note.user_id == user.id)
        )).scalars().all()
        by_title = {n.title: n for n in rows}
        new_user_id = user.id

    yield by_title, source

    async with async_session() as s:
        fresh = [await s.get(Note, r.id) for r in by_title.values()]
        for row in fresh:
            if row is not None:
                row.arose_from_id = None
                row.parent_id = None
        await s.flush()
        for row in fresh:
            if row is not None:
                await s.delete(row)
        await s.commit()
    async with async_session() as s:
        user = await s.get(User, new_user_id)
        if user is not None:
            await s.delete(user)
        await s.commit()


@pytest_asyncio.fixture(autouse=True)
async def _no_leftover_restored_user():
    """The payload's username is fixed, so a previous failed run would leave a
    row that makes `restored` pick the wrong user. Clear it first."""
    async with async_session() as s:
        stale = (await s.execute(
            select(User).where(User.username == RESTORED_USERNAME)
        )).scalars().all()
        for user in stale:
            notes = (await s.execute(
                select(Note).where(Note.user_id == user.id)
            )).scalars().all()
            for n in notes:
                n.arose_from_id = None
                n.parent_id = None
            await s.flush()
            for n in notes:
                await s.delete(n)
            await s.delete(user)
        await s.commit()


async def test_a_restored_record_keeps_what_it_IS(restored):
    """#3182's headline. Without note_type and task_kind a restore reported
    success and handed back a corpus where every snippet and process was a
    plain note and every issue and spike was `work` — the whole vocabulary
    milestone 312 and #3128 were about, gone, with nothing to notice it by."""
    by_title, _ = restored

    assert by_title["debounce — rate-limit"].note_type == "snippet"
    assert by_title["DRY pass"].note_type == "process"
    assert by_title["the fix"].task_kind == "issue"
    assert by_title["the work that broke"].task_kind == "work"


async def test_a_restored_snippet_keeps_its_queryable_mirror(restored):
    """The one field that would self-heal — backfill_snippet_data rebuilds it
    from the body at startup — but a restore should not hand back a corpus
    that needs a restart before it is findable by location."""
    by_title, _ = restored
    assert by_title["debounce — rate-limit"].data == {
        "name": "debounce", "language": "js",
    }


async def test_the_provenance_edge_is_remapped_not_copied(restored):
    """THE regression, and the reason this needed a real database.

    The payload's `arose_from_id` is an id in the SOURCE database. Copying it
    through would leave the restored issue pointing at whatever record happens
    to hold that number — a restore that succeeds and silently rewires
    history. The edge must land on the RESTORED origin instead.
    """
    by_title, src = restored
    issue = by_title["the fix"]
    origin = by_title["the work that broke"]

    assert issue.arose_from_id == origin.id
    # ...and that is a different row from the one the payload named.
    assert issue.arose_from_id != src["origin_id"]


async def test_description_and_status_survive(restored):
    by_title, _ = restored
    assert by_title["the fix"].description == "one-liner"
    assert by_title["the fix"].status == "done"
