/*------------------------------------------------------------------------
   File        : TestRepositionReader
   Purpose     : Theme 4 reader.  Uses a query handle rather than FOR EACH, to
                 establish whether repositioning discards a prefetched message and
                 forces a fresh server read.
   Notes       : Pairs with TestRepositionWriter on harness port 8894.

                 Rowids are captured in a pre-pass before the writer is
                 signalled.  That pass reads records this session has just created
                 anyway, so it cannot warm anything the main scan would not
                 already have warm.

                 RP01 and RP01b separate two questions that would otherwise be
                 confounded: does a query handle batch like FOR EACH, and does
                 declaring the query SCROLLING change batching on its own.

                 RP09 (GET PREV) and RP08 (BACKWARDS) are oracles for #11723's
                 "NEXT operations only" gate.
 ----------------------------------------------------------------------*/

using Progress.Lang.*.
using OpenEdge.Core.Assert.
using support.harness.msgbuf.* from propath.

block-level on error undo, throw.

class support.harness.msgbuf.TestRepositionReader:

    define variable sync    as MsgBufSync    no-undo.
    define variable fixture as MsgBufFixture no-undo.
    define variable log     as MsgBufLog     no-undo.
    define variable tgt     as MsgBufTargets no-undo.

    define query qFwd    for Book.
    define query qScroll for Book scrolling.

    /* Position 1..RecordCount in book-id order. */
    define variable rids as rowid extent 120 no-undo.

    @Before.
    method public void BeforeAll ():
        /* ABLUnit's @Before may run once per class or once per @Test depending on the
           release.  Written to be idempotent so the harness connection and the
           semantics probe happen exactly once either way - TestLockSession1.cls
           guards the same way. */
        if valid-object(sync) then
            return.

        session:suppress-warnings = false no-error.

        fixture = new MsgBufFixture().
        fixture:AssertClientServer().
        tgt = new MsgBufTargets().

        log = new MsgBufLog("reposition").
        log:StartRun(fixture:ConnectionInfo()).

        sync = new MsgBufSync().
        sync:Connect(8894, "TestRepositionReader").
    end method.

    @After.
    method public void AfterAll ():
        /* The dump is cumulative and idempotent, so it is safe whether @After runs
           once or after every @Test.  Deliberately does NOT disconnect: under
           per-test @After semantics that would tear down the lockstep after the
           first scenario.  The harness connection closes when the session exits. */
        if valid-object(log) then
            log:Dump("support/harness/msgbuf/results").
    end method.

    /* ---------------- baselines ---------------- */

    @Test.
    method public void RP01QueryHandleScan ():
        define variable iter as integer no-undo.

        Begin("RP01", "DEFINE QUERY (forward only) + GET NEXT", "blanket update -> V2").

        open query qFwd for each Book no-lock.
        get next qFwd.
        do while available Book:
            iter = iter + 1.
            log:Row(iter, Book.book-id, Book.new_dt).
            if iter eq tgt:PauseAt then
                sync:ReaderPause("RP01").
            get next qFwd.
        end.
        close query qFwd.

        EndScenario("RP01").
    end method.

    @Test.
    method public void RP01bScrollingQueryScan ():
        define variable iter as integer no-undo.

        /* Declaring the query SCROLLING may build a results list by itself, which
           would change batching before any reposition is involved. */
        Begin("RP01b", "DEFINE QUERY SCROLLING + GET NEXT", "blanket update -> V2").

        open query qScroll for each Book no-lock.
        get next qScroll.
        do while available Book:
            iter = iter + 1.
            log:Row(iter, Book.book-id, Book.new_dt).
            if iter eq tgt:PauseAt then
                sync:ReaderPause("RP01b").
            get next qScroll.
        end.
        close query qScroll.

        EndScenario("RP01b").
    end method.

    /* ---------------- reposition to rowid ---------------- */

    @Test.
    method public void RP02RepositionInsideBatch ():
        /* The core question of this theme. */
        ScanWithReposition("RP02", tgt:PauseAt + 1,
                           "reposition into the in-flight message", "blanket update -> V2").
    end method.

    @Test.
    method public void RP03RepositionBeyondBatch ():
        ScanWithReposition("RP03", 95,
                           "reposition beyond the in-flight message", "blanket update -> V2").
    end method.

    @Test.
    method public void RP04RepositionBackwards ():
        ScanWithReposition("RP04", 3,
                           "reposition backwards onto an already-visited row",
                           "blanket update -> V2").
    end method.

    @Test.
    method public void RP05RepositionToCurrent ():
        /* Does GET NEXT after this re-deliver the current row or the following one,
           and is the buffer refreshed either way? */
        ScanWithReposition("RP05", tgt:PauseAt,
                           "reposition onto the current row", "blanket update -> V2").
    end method.

    @Test.
    method public void RP06RepositionToDeletedRow ():
        ScanWithReposition("RP06", tgt:PauseAt + 1,
                           "reposition to a row the writer deleted",
                           substitute("delete id &1", tgt:KeyNext)).
    end method.

    @Test.
    method public void RP13RepositionToMovedRow ():
        /* rowid should be independent of the index key, so this should still
           resolve after the writer moves the record. */
        ScanWithReposition("RP13", tgt:PauseAt + 1,
                           "reposition to a row whose index key the writer moved",
                           substitute("move id &1 -> &2", tgt:KeyNext, tgt:KeyMoveAhead)).
    end method.

    /* ---------------- results-list navigation ---------------- */

    @Test.
    method public void RP07RepositionToRow ():
        define variable iter as integer no-undo.

        /* REPOSITION TO ROW needs a results list, so this runs on a preselect.
           Building that list is itself a prefetch-relevant act, which is the point
           rather than an obstacle. */
        Begin("RP07", "OPEN QUERY PRESELECT + REPOSITION TO ROW 50",
              "blanket update -> V2").

        open query qScroll preselect each Book no-lock by Book.book-id.
        NoteResultsList("after open").

        get next qScroll.
        do while available Book:
            iter = iter + 1.
            log:Row(iter, Book.book-id, Book.new_dt).

            if iter eq tgt:PauseAt then
                sync:ReaderPause("RP07").
            else if iter eq tgt:PauseAt + 1 then
            do:
                reposition qScroll to row 50 no-error.
                log:Note(substitute("after REPOSITION TO ROW 50: error=&1", error-status:error)).
                NoteResultsList("after reposition").
            end.

            get next qScroll.
        end.
        close query qScroll.

        EndScenario("RP07").
    end method.

    @Test.
    method public void RP08RepositionRelative ():
        define variable iter as integer no-undo.

        Begin("RP08", "REPOSITION FORWARDS 3 then BACKWARDS 2 mid-scan",
              "blanket update -> V2").

        open query qScroll for each Book no-lock.
        get next qScroll.
        do while available Book:
            iter = iter + 1.
            log:Row(iter, Book.book-id, Book.new_dt).

            if iter eq tgt:PauseAt then
                sync:ReaderPause("RP08").
            else if iter eq tgt:PauseAt + 1 then
            do:
                reposition qScroll forwards 3 no-error.
                log:Note(substitute("after REPOSITION FORWARDS 3: error=&1", error-status:error)).
            end.
            else if iter eq tgt:PauseAt + 3 then
            do:
                reposition qScroll backwards 2 no-error.
                log:Note(substitute("after REPOSITION BACKWARDS 2: error=&1", error-status:error)).
            end.

            get next qScroll.
        end.
        close query qScroll.

        EndScenario("RP08").
    end method.

    @Test.
    method public void RP09BackwardWalk ():
        define variable iter as integer no-undo.

        Begin("RP09", "GET NEXT to the pause, then GET PREV back to the start",
              "blanket update -> V2").

        open query qScroll for each Book no-lock.

        /* GET NEXT at the top of the block, so the scan fetches exactly PauseAt
           records and does not silently pull an unlogged one past the pause. */
        do while iter < tgt:PauseAt:
            get next qScroll.
            if not available Book then
                leave.
            iter = iter + 1.
            log:Row(iter, Book.book-id, Book.new_dt, "dir=forward").
        end.

        sync:ReaderPause("RP09").

        get prev qScroll.
        do while available Book:
            iter = iter + 1.
            log:Row(iter, Book.book-id, Book.new_dt, "dir=backward").
            get prev qScroll.
        end.
        close query qScroll.

        EndScenario("RP09").
    end method.

    @Test.
    method public void RP10GetFirstAndReopen ():
        define variable iter as integer no-undo.

        /* Control for the whole theme: if the writer's commit is not visible even
           after a re-open, nothing else in this file means anything. */
        Begin("RP10", "GET FIRST, then a full re-OPEN of the query",
              "blanket update -> V2").

        open query qScroll for each Book no-lock.

        do while iter < tgt:PauseAt:
            get next qScroll.
            if not available Book then
                leave.
            iter = iter + 1.
            log:Row(iter, Book.book-id, Book.new_dt).
        end.

        sync:ReaderPause("RP10").

        get first qScroll.
        iter = iter + 1.
        log:Row(iter, Book.book-id, Book.new_dt, "phase=after-get-first").

        open query qScroll for each Book no-lock.
        get next qScroll.
        iter = iter + 1.
        log:Row(iter, Book.book-id, Book.new_dt, "phase=after-reopen").
        close query qScroll.

        EndScenario("RP10").
    end method.

    @Test.
    method public void RP12ResultsListUnderChange ():
        define variable iter as integer no-undo.

        Begin("RP12", "NUM-RESULTS / CURRENT-RESULT-ROW across a create and a delete",
              substitute("create id &1 and delete id &2",
                         tgt:KeyCreateBeyond, tgt:KeyFar)).

        open query qScroll preselect each Book no-lock by Book.book-id.
        NoteResultsList("after open, before writer").

        get next qScroll.
        do while available Book:
            iter = iter + 1.
            log:Row(iter, Book.book-id, Book.new_dt).

            if iter eq tgt:PauseAt then
            do:
                sync:ReaderPause("RP12").
                NoteResultsList("after writer create+delete").
            end.

            get next qScroll.
        end.
        close query qScroll.

        EndScenario("RP12").
    end method.

    /* ---------------- dynamic handle form ---------------- */

    @Test.
    method public void RP11DynamicQueryHandle ():
        define variable hQry  as handle  no-undo.
        define variable hBuf  as handle  no-undo.
        define variable iter  as integer no-undo.
        define variable moved as logical no-undo.

        /* Same probe as RP02 through the handle API, to confirm the static and
           dynamic forms agree. */
        Begin("RP11", "CREATE QUERY + QUERY-PREPARE + GET-NEXT + REPOSITION-TO-ROWID",
              "blanket update -> V2").

        create buffer hBuf for table "Book".
        create query hQry.
        hQry:set-buffers(hBuf).
        hQry:query-prepare("for each Book no-lock").
        hQry:query-open().

        hQry:get-next().
        do while hBuf:available:
            iter = iter + 1.
            log:Row(iter,
                    integer(hBuf:buffer-field("book-id"):buffer-value),
                    string(hBuf:buffer-field("new_dt"):buffer-value)).

            if iter eq tgt:PauseAt then
                sync:ReaderPause("RP11").
            else if iter eq tgt:PauseAt + 1 and not moved then
            do:
                moved = true.
                hQry:reposition-to-rowid(rids[tgt:PauseAt + 1]) no-error.
                log:Note(substitute("after REPOSITION-TO-ROWID(pos &1): available=&2 error=&3",
                                    tgt:PauseAt + 1, hBuf:available, error-status:error)).
            end.

            hQry:get-next().
        end.

        hQry:query-close().

        EndScenario("RP11").

        finally:
            delete object hQry no-error.
            delete object hBuf no-error.
        end finally.
    end method.

    /* ---------------- shared scaffolding ---------------- */

    /* Scan on the SCROLLING query, stop, let the writer act, then reposition to a
       captured rowid and keep going.  Everything logged after the reposition says
       whether the message survived it. */
    method private void ScanWithReposition (scn as character, targetPos as integer,
                                          what as character, writerAction as character):
        define variable iter  as integer no-undo.
        define variable moved as logical no-undo.

        Begin(scn, substitute("DEFINE QUERY SCROLLING + GET NEXT, then &1", what),
              writerAction).

        open query qScroll for each Book no-lock.
        get next qScroll.
        do while available Book:
            iter = iter + 1.
            log:Row(iter, Book.book-id, Book.new_dt).

            if iter eq tgt:PauseAt then
                sync:ReaderPause(scn).
            else if iter eq tgt:PauseAt + 1 and not moved then
            do:
                moved = true.

                reposition qScroll to rowid rids[targetPos] no-error.
                log:Note(substitute("after REPOSITION TO ROWID(pos &1, id &2): available=&3 error=&4",
                                    targetPos, fixture:KeyAt(targetPos),
                                    available Book, error-status:error)).

                if available Book then
                    log:Note(substitute("buffer immediately after reposition: id=&1 mark=&2",
                                        Book.book-id, Book.new_dt)).
            end.

            get next qScroll.
        end.
        close query qScroll.

        EndScenario(scn).
    end method.

    method private void Begin (scn as character, queryText as character,
                              writerAction as character):
        fixture:Build(fixture:PROFILE_SLIM).
        CaptureRowids().
        log:StartScenario(scn, fixture:PROFILE_SLIM, fixture:SampleRecordLength(),
                          tgt:PauseAt, queryText, writerAction).
        sync:ReaderReady(scn).
    end method.

    method private void CaptureRowids ():
        define variable i as integer no-undo.

        for each Book no-lock by Book.book-id:
            i = i + 1.
            if i <= extent(rids) then
                rids[i] = rowid(Book).
        end.

        release Book.
    end method.

    method private void NoteResultsList (phase as character):
        log:Note(substitute("&1: NUM-RESULTS=&2 CURRENT-RESULT-ROW=&3",
                            phase,
                            num-results("qScroll"),
                            current-result-row("qScroll"))).
    end method.

    method private void EndScenario (scn as character):
        sync:ReaderDone(scn).
        log:EndScenario().

        /* Repositioning changes how many rows a scan yields on purpose, so the
           guard is only that the reader got past its own pause. */
        Assert:IsTrue(log:ScenarioRows > tgt:PauseAt).
    end method.

end class.
