"""Body provenance — the cache-with-provenance half of the pointer model (#2688).

Decision #2686: the recorded location is the source of truth for a snippet's
code and the stored body is a CACHE of it. `data.provenance` records what that
cache is a cache OF — the commit the body was read at, and when — so staleness
becomes judgeable instead of guessed, and so the forge fetch (milestone 288
step 5) has something to refresh.

The rules under test, because each has a way to rot silently:

  - Provenance follows the CODE. An edit that changes the code without a fresh
    SHA must DROP the stamp — carrying it would claim the new body came from
    the old commit, which is worse than not knowing.
  - Writes that are ABOUT the code rather than changes TO it (a verification
    verdict, a metadata edit) must CARRY it — record_verification rebuilds
    `data` from scratch, so forgetting the field there erases it invisibly.
  - An "ok" verdict at a known commit RESTAMPS it: the checker just proved the
    cached body matches the source there.

Unit tests cover the compose/carry logic; the integration section runs the
same rules through the real service paths on real Postgres (the #2663 lesson —
a DB-touching path with only mocked coverage is a path with no coverage).
"""
import pytest
import pytest_asyncio

from tests.helpers import ensure_user

from scribe.services.snippets import (
    VERIFY_CHANGED,
    VERIFY_OK,
    compose_data,
    compose_provenance,
    compose_verification,
    snippet_fields,
    verification_view,
)

SHA_A = "a" * 40
SHA_B = "b" * 40


# --- unit: composing ---------------------------------------------------------

def test_compose_provenance_stamps_sha_and_time():
    prov = compose_provenance(commit_sha=f"  {SHA_A} ")
    assert prov["commit_sha"] == SHA_A
    assert prov["fetched_at"]  # ISO stamp, defaulted


def test_compose_provenance_without_a_sha_is_none_not_an_empty_record():
    # Absent provenance must stay ABSENT (the pre-#2688 semantics), never an
    # empty dict that readers would have to distinguish from a real one.
    assert compose_provenance(commit_sha="") is None
    assert compose_provenance(commit_sha="   ") is None


def test_compose_data_carries_provenance_only_when_present():
    with_it = compose_data(name="x", provenance={"commit_sha": SHA_A, "fetched_at": "t"})
    without = compose_data(name="x", provenance=None)
    assert with_it["provenance"]["commit_sha"] == SHA_A
    assert "provenance" not in without


def test_verification_records_and_reads_back_the_checked_commit():
    verdict = compose_verification(
        status=VERIFY_OK, checked_code_sha="c" * 32, commit_sha=SHA_A,
    )
    assert verdict["commit_sha"] == SHA_A
    # And an empty one is omitted, not stored as "".
    bare = compose_verification(status=VERIFY_OK, checked_code_sha="c" * 32)
    assert "commit_sha" not in bare

    class _N:  # minimal note stand-in for the read-time view
        data = None

    fields = {"code": "", "verification": verdict}
    view = verification_view(_N(), fields)
    assert view["commit_sha"] == SHA_A


# --- integration: the rules through the real service paths -------------------


@pytest_asyncio.fixture
async def user_id(_dispose_engine):
    from scribe.models import async_session

    async with async_session() as s:
        uid = (await ensure_user(s, "snippet_prov_itest")).id
        await s.commit()
    return uid


async def _fresh(uid, note_id):
    from scribe.services import snippets as svc
    note = await svc.get_snippet(uid, note_id)
    return snippet_fields(note), svc.snippet_to_dict(note)


@pytest.mark.integration
async def test_provenance_lives_and_dies_with_the_code_end_to_end(user_id):
    from scribe.services import snippets as svc

    note = await svc.create_snippet(
        user_id, name="prov_helper", code="def prov_helper():\n    return 1\n",
        language="python", repo="Scribe", path="src/x.py", symbol="prov_helper",
        commit_sha=SHA_A,
    )
    fields, view = await _fresh(user_id, note.id)
    assert fields["provenance"]["commit_sha"] == SHA_A
    assert view["snippet"]["provenance"]["commit_sha"] == SHA_A

    # A metadata edit leaves the code alone → carried.
    await svc.update_snippet(user_id, note.id, when_to_use="when proving")
    fields, _ = await _fresh(user_id, note.id)
    assert fields["provenance"]["commit_sha"] == SHA_A

    # A code edit with a fresh SHA → restamped.
    await svc.update_snippet(
        user_id, note.id, code="def prov_helper():\n    return 2\n",
        commit_sha=SHA_B,
    )
    fields, _ = await _fresh(user_id, note.id)
    assert fields["provenance"]["commit_sha"] == SHA_B

    # A code edit WITHOUT one → dropped, not carried: the new body does not
    # come from SHA_B and the record must not claim it does.
    await svc.update_snippet(
        user_id, note.id, code="def prov_helper():\n    return 3\n",
    )
    fields, view = await _fresh(user_id, note.id)
    assert "provenance" not in fields
    assert "provenance" not in view["snippet"]


@pytest.mark.integration
async def test_verification_stamps_the_commit_and_ok_refreshes_provenance(user_id):
    from scribe.services import snippets as svc

    note = await svc.create_snippet(
        user_id, name="prov_verify", code="def prov_verify():\n    return 1\n",
        language="python", repo="Scribe", path="src/y.py", symbol="prov_verify",
        commit_sha=SHA_A,
    )

    # A non-ok verdict at a newer commit records where the check ran but must
    # CARRY provenance — the check didn't change what the cached body is.
    await svc.record_verification(
        user_id, note.id, status=VERIFY_CHANGED, detail="diverged", commit_sha=SHA_B,
    )
    fields, view = await _fresh(user_id, note.id)
    assert view["verification"]["commit_sha"] == SHA_B
    assert fields["provenance"]["commit_sha"] == SHA_A

    # An OK verdict at that commit proves the cache matches the source there —
    # provenance refreshes without an edit.
    await svc.record_verification(
        user_id, note.id, status=VERIFY_OK, detail="matches", commit_sha=SHA_B,
    )
    fields, view = await _fresh(user_id, note.id)
    assert view["verification"]["commit_sha"] == SHA_B
    assert fields["provenance"]["commit_sha"] == SHA_B

    # And the verdict itself survives untouched by the restamp.
    assert view["verification"]["status"] == VERIFY_OK
    assert view["verification"]["current"] is True
