/**
 * @module sync-conflict.respec
 *
 * Verifies Conflict Resolution strategies in SyncSagaCoordinator.
 * Reproduces divergence scenarios and asserts resolution behavior.
 */
import { respecfully, ifWe, iReckon } from '@ibgib/helper-gib/dist/respec-gib/respec-gib.mjs';
const maam = `[${import.meta.url}]`, sir = maam;
import { clone, delay, extractErrorMsg, pretty } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
import { getIbGibAddr } from '@ibgib/ts-gib/dist/helper.mjs';
import { SyncSagaCoordinator } from './sync-saga-coordinator.mjs';
import { getFromSpace } from '../witness/space/space-helper.mjs';
import { Metaspace_Innerspace } from '../witness/space/metaspace/metaspace-innerspace/metaspace-innerspace.mjs';
import { InnerSpace_V1 } from '../witness/space/inner-space/inner-space-v1.mjs';
import { createTimelineRootTestHelper, } from '../test-helpers.mjs';
import { mut8Timeline } from '../timeline/timeline-api.mjs';
import { SyncPeerInnerspace_V1 } from './sync-peer/sync-peer-innerspace/sync-peer-innerspace-v1.mjs';
import { DEFAULT_INNER_SPACE_DATA_V1 } from '../witness/space/inner-space/inner-space-types.mjs';
import { getTjpAddr } from '../common/other/ibgib-helper.mjs';
import { fnObs } from '../common/pubsub/observer/observer-helper.mjs';
import { getDependencyGraph } from '../common/other/graph-helper.mjs';
import { SYNC_PEER_INNERSPACE_DEFAULT_DATA_V1 } from './sync-peer/sync-peer-innerspace/sync-peer-innerspace-constants.mjs';
import { SyncConflictStrategy } from './sync-constants.mjs';
import { GRAFT_BASE_REL8N_NAME, GRAFT_INFO_REL8N_NAME, GRAFT_ORPHAN_REL8N_NAME } from './graft-info/graft-info-constants.mjs';
const logalot = false;
const lc = sir;
await respecfully(sir, `Two different fields`, async () => {
    // run it twice, once where the source makes the divergent edit first, and a
    // second time where the dest makes the edit. strictly speaking, we should
    // rig a third case where they are made at exactly the same time, but it's
    // an edge case and looking at code it compares <= ticks so not really worth
    // it.
    for (let i = 0; i <= 1; i++) {
        const srcMakesFirstDivergentEdit = i === 0;
        await respecfully(sir, `srcMakesFirstDivergentEdit: ${srcMakesFirstDivergentEdit}`, async () => {
            // 1. Setup Spaces
            const metaspace = new Metaspace_Innerspace(undefined);
            await metaspace.initialize({
                getFnAlert: () => async ({ title, msg }) => { console.log(`[Alert] ${title}: ${msg}`); },
                getFnPrompt: () => async ({ title, msg }) => { console.log(`[Prompt] ${title}: ${msg}`); return ''; },
                getFnPromptPassword: () => async (title, msg) => { console.log(`[PromptPwd] ${title}: ${msg}`); return null; },
            });
            while (!metaspace.initialized) {
                await delay(10);
            }
            const defaultLocalUserSpace = await metaspace.getLocalUserSpace({ lock: false });
            await defaultLocalUserSpace.initialized;
            const sourceSpace = new InnerSpace_V1({
                ...DEFAULT_INNER_SPACE_DATA_V1,
                name: 'source',
                uuid: 'source_uuid',
                description: 'source test space',
            });
            await sourceSpace.initialized;
            const destSpace = new InnerSpace_V1({
                ...DEFAULT_INNER_SPACE_DATA_V1,
                name: 'dest',
                uuid: 'dest_uuid',
                description: 'dest test space',
            });
            await destSpace.initialized;
            // 2. Seed Common History (V0 -> V1)
            console.log(`${lc} Creating Common History...`);
            const testRoot = await createTimelineRootTestHelper({
                ib: 'timeline_root',
                data: { type: 'testRoot', text: 'v0' },
                space: sourceSpace,
            });
            // Create V1 (Common)
            const v1_Common_onSource = await mut8Timeline({
                timeline: testRoot,
                mut8Opts: {
                    mut8Ib: testRoot.ib + '_common',
                    dataToAddOrPatch: { text: 'v1_common' }
                },
                metaspace,
                space: sourceSpace,
            });
            const tjpAddr = getTjpAddr({ ibGib: testRoot, defaultIfNone: 'incomingAddr' }) ??
                getIbGibAddr({ ibGib: testRoot });
            // Sync testRoot to dest immediately so they start synced
            // must get the entire dependency graph for testRoot
            const depGraph_testRootAndV1Common = await getDependencyGraph({
                ibGibAddrs: [tjpAddr],
                space: sourceSpace,
                live: true,
            }) ?? {};
            console.log(`${lc} depGraph_testRootAndV1Common: ${pretty(depGraph_testRootAndV1Common)}`);
            if (Object.keys(depGraph_testRootAndV1Common).length === 0) {
                throw new Error(`(UNEXPECTED) depGraph_testRootAndV1Common empty? (E: 39b4d855ffa65476084b4123786da826)`);
            }
            // put the entire graph into the destspace, but...
            await metaspace.put({ ibGibs: Object.values(depGraph_testRootAndV1Common), space: destSpace });
            const resGetDest = await getFromSpace({ space: destSpace, addr: getIbGibAddr({ ibGib: v1_Common_onSource }) });
            if (!resGetDest.success || !resGetDest.ibGibs || resGetDest.ibGibs.length === 0) {
                throw new Error(`Failed to retrieve v1_Common_onDest from destSpace. (E: 40c67811eba14f729880a46dcbb65126)`);
            }
            /**
             * same as v1_common
             */
            const v1_Common_onDest = resGetDest.ibGibs[0];
            // ...only register the main ibgibs
            await metaspace.registerNewIbGib({ ibGib: testRoot, space: destSpace });
            await metaspace.registerNewIbGib({ ibGib: v1_Common_onDest, space: destSpace }); // it's the "on source" one, but only because
            // 3. Create Divergence (V2a vs V2b)
            console.log(`${lc} Creating Divergence...`);
            let v2a;
            let v2b;
            let baseAddr;
            let orphanAddr;
            if (srcMakesFirstDivergentEdit) {
                // Source: V1 -> V2a (Edit Field A)
                v2a = await mut8Timeline({
                    timeline: v1_Common_onSource,
                    mut8Opts: {
                        mut8Ib: testRoot.ib + '_v2a',
                        dataToAddOrPatch: { fieldA: 'source_edit' }
                    },
                    metaspace,
                    space: sourceSpace,
                });
                /**
                 * since we're doing this first, this will be the base in our graft
                 */
                baseAddr = getIbGibAddr({ ibGib: v2a });
                /**
                 * timestamp/timestampMs drives orphan/base timeline selection in
                 * graftTimelines
                 */
                await delay(16);
                // Dest: V1 -> V2b (Edit Field B)
                v2b = await mut8Timeline({
                    timeline: v1_Common_onDest,
                    mut8Opts: {
                        mut8Ib: testRoot.ib + '_v2b',
                        dataToAddOrPatch: { fieldB: 'dest_edit' }
                    },
                    metaspace,
                    space: destSpace,
                });
                /**
                 * since we're doing this second (i.e., later temporally), this will be
                 * the orphan in our graft
                 */
                orphanAddr = getIbGibAddr({ ibGib: v2b });
            }
            else {
                // Dest: V1 -> V2b (Edit Field B)
                v2b = await mut8Timeline({
                    timeline: v1_Common_onDest,
                    mut8Opts: {
                        mut8Ib: testRoot.ib + '_v2b',
                        dataToAddOrPatch: { fieldB: 'dest_edit' }
                    },
                    metaspace,
                    space: destSpace,
                });
                /**
                 * since we're doing this first, this will be the base in our graft
                 */
                baseAddr = getIbGibAddr({ ibGib: v2b });
                /**
                 * timestamp/timestampMs drives orphan/base timeline selection in
                 * graftTimelines
                 */
                await delay(16);
                // Source: V1 -> V2a (Edit Field A)
                v2a = await mut8Timeline({
                    timeline: v1_Common_onSource,
                    mut8Opts: {
                        mut8Ib: testRoot.ib + '_v2a',
                        dataToAddOrPatch: { fieldA: 'source_edit' }
                    },
                    metaspace,
                    space: sourceSpace,
                });
                /**
                 * since we're doing this second (i.e., later temporally), this will be
                 * the orphan in our graft
                 */
                orphanAddr = getIbGibAddr({ ibGib: v2a });
            }
            // Verify Divergence
            console.log(`${lc} Divergence created.`);
            // 4. Run Sync (Optimistic)
            console.log(`${lc} Setting up Coordinators...`);
            const senderCoordinator = new SyncSagaCoordinator();
            const receiverCoordinator = new SyncSagaCoordinator();
            const peer = new SyncPeerInnerspace_V1(clone(SYNC_PEER_INNERSPACE_DEFAULT_DATA_V1));
            await peer.initialized;
            await peer.initializeOpts({
                localMetaspace: metaspace,
                localSpace: sourceSpace, // "Client"
                receiverSpace: destSpace, // "Server"
                receiverCoordinator,
                receiverMetaspace: metaspace,
            });
            // Verify Receiver has correct KV (Pre-Sync Check)
            // This ensures the conflict precondition exists.
            await ifWe(sir, 'verify receiver KV pre-sync', async () => {
                try {
                    const destKV = await receiverCoordinator.getKnowledgeMap({
                        space: destSpace,
                        metaspace,
                        domainIbGibs: [v1_Common_onDest]
                    });
                    const v1TjpAddr = getTjpAddr({ ibGib: v1_Common_onDest, defaultIfNone: 'incomingAddr' }) ?? getIbGibAddr({ ibGib: v1_Common_onDest });
                    // Actually v1 has tjp rel8n.
                    const destTip = destKV[v1TjpAddr];
                    iReckon(sir, !!destTip).asTo(`Dest KV has timeline tip for ${v1TjpAddr}`).isGonnaBeTruthy();
                    if (!destTip) {
                        throw new Error(`Test Setup Fail: Dest Space does not have index for timeline ${v1TjpAddr}. Seeding failed. (E: c194a80b4e4877b77826c37a1753b826)`);
                    }
                }
                catch (error) {
                    console.error(`${lc} ${extractErrorMsg(error)}`);
                    iReckon(sir, true).asTo('getKnowledgeMap errored out').isGonnaBeFalse();
                }
            });
            console.log(`${lc} Running Sync (ConflictStrategy: optimistic)...`);
            let resSync;
            try {
                console.dir(sourceSpace);
                console.dir(destSpace);
                resSync = await senderCoordinator.sync({
                    peer,
                    localSpace: sourceSpace,
                    metaspace,
                    domainIbGibs: [v2a],
                    conflictStrategy: SyncConflictStrategy.optimisticWithLCS,
                });
                const sublc = `${lc}[updates$]`;
                /**
                 * I have added this so you can see how to subscribe to an ibgib
                 * observable using {@link fnObs}.
                 */
                const subscription = await resSync.updates$.subscribe(fnObs({
                    next: async (ctxIbGib) => {
                        // console.log(`${sublc} next fired. ${JSON.stringify(ctxIbGib)}`);
                        console.log(`${sublc} next fired. (I: e68d8894bac8800f9f3430e8a38d6626)`);
                    },
                    error: async (e) => {
                        if (e.data) {
                            console.error(`${sublc} error fired. error: ${JSON.stringify(e.data)} (E: eddf17f76a486b9c5a2f4ee86ed38b26)`);
                        }
                        else {
                            console.dir(e);
                            console.error(`${sublc} error fired. error: ${extractErrorMsg(e)} (E: af9c3b6f1c88befeff77ca46111b3826)`);
                        }
                    },
                    complete: async () => {
                        console.log(`${sublc} complete fired`);
                    },
                }));
                console.log(`${lc} awaiting resSync.done`);
                await resSync.done;
                console.log(`${lc} Sync Complete.`);
            }
            catch (e) {
                console.error(`${lc} Sync Failed with Error:`, e);
                iReckon(sir, false).asTo(`Sync failed with error: ${e}`).isGonnaBeTruthy();
                return; // Exit test early
            }
            // 5. Verification
            // Expectation: Both Spaces should now have a NEW tip (V3) that merges V2a and V2b.
            const resSourceTip = await getFromSpace({ space: sourceSpace, addr: getIbGibAddr({ ibGib: v2a }) });
            await ifWe(sir, `verify merge`, async () => {
                // Retrieve updated KV from Source (or check what happened to v2a)
                // Ideally, we just check the source space for the timeline tip
                // The timeline tip for testRoot/v0 should now be NEW.
                try {
                    // Get the KV for the Source Space
                    const sourceKV = await senderCoordinator.getKnowledgeMap({
                        space: sourceSpace,
                        metaspace,
                        domainIbGibs: [testRoot] // We want to know the tip of this timeline
                    });
                    const tjpAddr = getTjpAddr({ ibGib: testRoot, defaultIfNone: 'incomingAddr' }) ??
                        getIbGibAddr({ ibGib: testRoot });
                    if (logalot) {
                        console.log(`${lc} getKnowledgeMap returned. sourceKV: ${pretty(sourceKV)} (I: e8780cda37c8b2a46eeb85786874e926)`);
                    }
                    const sourceTipAddr = sourceKV[tjpAddr];
                    if (!sourceTipAddr) {
                        throw new Error(`Source Space missing timeline tip for ${tjpAddr} (E: ec95980b9c980c5c5870812e15e43826)`);
                    }
                    iReckon(sir, sourceTipAddr).asTo(`Source Tip (${sourceTipAddr}) should NOT be v2a`).not.isGonnaBe(getIbGibAddr({ ibGib: v2a }));
                    iReckon(sir, sourceTipAddr).asTo(`Source Tip (${sourceTipAddr}) should NOT be v2b`).not.isGonnaBe(getIbGibAddr({ ibGib: v2b }));
                    // Fetch the new tip
                    const resTip = await getFromSpace({ space: sourceSpace, addr: sourceTipAddr });
                    const newTip = resTip.ibGibs[0];
                    // Check Data: Should have BOTH edits
                    iReckon(sir, newTip.data.fieldA).asTo('New Tip Data Field A').isGonnaBe('source_edit');
                    iReckon(sir, newTip.data.fieldB).asTo('New Tip Data Field B').isGonnaBe('dest_edit');
                    // Check Graft Structure
                    // Look for 'graftinfo' rel8n
                    if (!newTip.rel8ns) {
                        throw new Error(`(UNEXPECTED) newTip.rel8ns falsy? (E: eb4078712ea2f02c7ee84b983ed77826)`);
                    }
                    const graftInfoRel = newTip.rel8ns[GRAFT_INFO_REL8N_NAME];
                    iReckon(sir, graftInfoRel).asTo('New Tip should have graftinfo rel8n').isGonnaBeTruthy();
                    if (!graftInfoRel) {
                        return; /* <<<< returns early, above reckoning fails */
                    }
                    iReckon(sir, graftInfoRel.length).asTo('Only 1 graftinfo').isGonnaBe(1);
                    // Fetch Graft Info Stone
                    const resGraft = await getFromSpace({ space: sourceSpace, addr: graftInfoRel[0] });
                    const graftInfo = resGraft.ibGibs[0];
                    // Check Graft Relations (graftbase, graftorphan)
                    if (!graftInfo.rel8ns) {
                        throw new Error(`(UNEXPECTED) graftInfo.rel8ns falsy? (E: 8e39e84e9e0813e09e8f6788ce488926)`);
                    }
                    const baseRel = graftInfo.rel8ns[GRAFT_BASE_REL8N_NAME];
                    const orphanRel = graftInfo.rel8ns[GRAFT_ORPHAN_REL8N_NAME];
                    iReckon(sir, baseRel).asTo('Graft Base exists').isGonnaBeTruthy();
                    iReckon(sir, baseRel.length).asTo('Graft Base length is 1').isGonnaBe(1);
                    iReckon(sir, orphanRel).asTo('Graft Orphan exists').isGonnaBeTruthy();
                    iReckon(sir, orphanRel.length).asTo('Graft Orphan length is 1').isGonnaBe(1);
                    iReckon(sir, baseRel[0]).asTo('base rel8ns will be earlier addr').isGonnaBe(baseAddr);
                    iReckon(sir, orphanRel[0]).asTo('orphan rel8ns will be later addr').isGonnaBe(orphanAddr);
                }
                catch (error) {
                    console.error(`${lc} ${extractErrorMsg(error)}`);
                    iReckon(sir, true).asTo('getKnowledgeMap errored out').isGonnaBeFalse();
                }
            });
        });
    }
});
//# sourceMappingURL=sync-conflict-basic-divergence.respec.mjs.map