/**
 * @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, getTestKeystoneServiceHelper } from '../test-helpers.mjs';
import { appendToTimeline, 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 { getIbGibsFromCache_fallbackToSpaces, getTjpAddr } from '../common/other/ibgib-helper.mjs';
import { fnObs } from '../common/pubsub/observer/observer-helper.mjs';
import { getDependencyGraph, graphsAreEquivalent } 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;
const TEST_REL8N_NAME = 'testrel8n';
await respecfully(sir, `Two different fields and rel8d`, 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 alpha_v0 = await createTimelineRootTestHelper({
        /**
         * greek letters are distinct timelines
         */
        ib: 'alpha',
        data: { type: 'alpha', },
        space: sourceSpace,
    });
    // Create v1 (common)
    /**
     * **created** on source, but sync will take it to dest
     */
    const alpha_v1_common_onSource = await mut8Timeline({
        timeline: alpha_v0,
        mut8Opts: {
            mut8Ib: alpha_v0.ib + '_common',
            dataToAddOrPatch: { commonField: 'common yo' }
        },
        metaspace,
        space: sourceSpace,
    });
    const tjpAddr = getTjpAddr({ ibGib: alpha_v0, defaultIfNone: 'incomingAddr' }) ??
        getIbGibAddr({ ibGib: alpha_v0 });
    // Sync alpha to dest immediately so they start synced
    // must get the entire dependency graph for alpha
    const depGraph_alpha_v1Andcommon = await getDependencyGraph({
        ibGibAddrs: [tjpAddr],
        space: sourceSpace,
        live: true,
    }) ?? {};
    console.log(`${lc} depGraph_testRootAndV1Common: ${pretty(depGraph_alpha_v1Andcommon)}`);
    if (Object.keys(depGraph_alpha_v1Andcommon).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_alpha_v1Andcommon), space: destSpace });
    const resGetDest = await getFromSpace({ space: destSpace, addr: getIbGibAddr({ ibGib: alpha_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 alpha_v1_common_onDest = resGetDest.ibGibs[0];
    // ...only register the main ibgibs
    await metaspace.registerNewIbGib({ ibGib: alpha_v0, space: destSpace });
    await metaspace.registerNewIbGib({ ibGib: alpha_v1_common_onDest, space: destSpace }); // it's the "on source" one, but only because
    // 3. Create Divergence(s) (v2source vs v2dest), i.e. "simultaneous" parallel edits
    // NOTE: source vs dest here is to indicate the location the edit is made.
    // syncing will obviously blur this line.
    console.log(`${lc} Creating Divergence...`);
    // #region edit on source
    // Source: v1 (common) -> v2source (Edit Field A and rel8 beta)
    const beta_v0_source = await createTimelineRootTestHelper({
        ib: 'beta',
        data: {
            fieldA: 'beta field A created on source',
        },
        space: sourceSpace,
    });
    const betaAddr_v0 = getIbGibAddr({ ibGib: beta_v0_source });
    const alpha_v2_source_sansBeta = await mut8Timeline({
        timeline: alpha_v1_common_onSource,
        mut8Opts: {
            mut8Ib: alpha_v0.ib + '_v2source',
            dataToAddOrPatch: { fieldA: 'source_edit' }
        },
        metaspace,
        space: sourceSpace,
    });
    const alpha_v3_source_rel8beta = await appendToTimeline({
        timeline: alpha_v2_source_sansBeta,
        metaspace,
        space: sourceSpace,
        rel8nInfos: [
            { rel8nName: TEST_REL8N_NAME, ibGibs: [beta_v0_source] }
        ],
    });
    /**
     * since we're doing this first, this will be the base in our graft
     */
    const baseAddr = getIbGibAddr({ ibGib: alpha_v3_source_rel8beta });
    // #endregion edit on source
    /**
     * timestamp/timestampMs drives orphan/base timeline selection in
     * graftTimelines
     */
    await delay(16);
    // #region edit on dest
    // Dest: v1 (common) -> alpha_v2_dest (Edit Field B)
    const alpha_v2_dest = await mut8Timeline({
        timeline: alpha_v1_common_onDest,
        mut8Opts: {
            mut8Ib: alpha_v0.ib + '_v2dest',
            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
     */
    const orphanAddr = getIbGibAddr({ ibGib: alpha_v2_dest });
    // Verify Divergence
    console.log(`${lc} Divergence created.`);
    // 4. Run Sync (Optimistic)
    console.log(`${lc} Setting up Coordinators...`);
    const mockKeystone = await getTestKeystoneServiceHelper();
    const senderCoordinator = new SyncSagaCoordinator(mockKeystone);
    const receiverCoordinator = new SyncSagaCoordinator(mockKeystone);
    const peer = new SyncPeerInnerspace_V1(clone(SYNC_PEER_INNERSPACE_DEFAULT_DATA_V1));
    await peer.initialized;
    await peer.initializeSender({
        senderSpace: 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: [alpha_v1_common_onDest]
            });
            const v1TjpAddr = getTjpAddr({ ibGib: alpha_v1_common_onDest, defaultIfNone: 'incomingAddr' }) ?? getIbGibAddr({ ibGib: alpha_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: [alpha_v3_source_rel8beta],
            conflictStrategy: SyncConflictStrategy.optimisticWithLCS,
            useSessionIdentity: false,
        });
        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 v2source and v2dest.
    const resSourceTip = await getFromSpace({ space: sourceSpace, addr: getIbGibAddr({ ibGib: alpha_v3_source_rel8beta }) });
    await respecfully(sir, `verify merge`, async () => {
        // Retrieve updated KV from Source (or check what happened to v2source)
        // Ideally, we just check the source space for the timeline tip
        // The timeline tip for alpha/v0 should now be NEW.
        try {
            // Get the KV for the Source Space
            const sourceKV_afterSync = await senderCoordinator.getKnowledgeMap({
                space: sourceSpace,
                metaspace,
                domainIbGibs: [alpha_v0] // We want to know the tip of this timeline
            });
            const tjpAddr = getTjpAddr({ ibGib: alpha_v0, defaultIfNone: 'incomingAddr' }) ??
                getIbGibAddr({ ibGib: alpha_v0 });
            if (logalot) {
                console.log(`${lc} getKnowledgeMap returned. sourceKV_afterSync: ${pretty(sourceKV_afterSync)} (I: e8780cda37c8b2a46eeb85786874e926)`);
            }
            const sourceTipAddr = sourceKV_afterSync[tjpAddr];
            if (!sourceTipAddr) {
                throw new Error(`Source Space missing timeline tip for ${tjpAddr} (E: ec95980b9c980c5c5870812e15e43826)`);
            }
            // Fetch the new tip
            const resTip = await getFromSpace({ space: sourceSpace, addr: sourceTipAddr });
            const newTip = resTip.ibGibs[0];
            await ifWe(sir, 'basics of alpha merge', async () => {
                iReckon(sir, sourceTipAddr).asTo(`Source Tip (${sourceTipAddr}) should NOT be alpha_v3_source_rel8beta`).not.isGonnaBe(getIbGibAddr({ ibGib: alpha_v3_source_rel8beta }));
                iReckon(sir, sourceTipAddr).asTo(`Source Tip (${sourceTipAddr}) should NOT be v2dest`).not.isGonnaBe(getIbGibAddr({ ibGib: alpha_v2_dest }));
                // 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
            await ifWe(sir, 'graft info is valid', async () => {
                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 2').isGonnaBe(2);
                iReckon(sir, orphanRel).asTo('Graft Orphan exists').isGonnaBeTruthy();
                iReckon(sir, orphanRel.length).asTo('Graft Orphan length is 1').isGonnaBe(1);
                iReckon(sir, baseRel.at(-1)).asTo('base rel8ns final addr will be baseAddr').isGonnaBe(baseAddr);
                iReckon(sir, orphanRel[0]).asTo('orphan rel8ns will be later addr').isGonnaBe(orphanAddr);
            });
            await ifWe(sir, 'alpha and deps synced', async () => {
                // alpha's full dep graph should exist on dest
                const [beta_dest] = await getIbGibsFromCache_fallbackToSpaces({
                    addrs: [betaAddr_v0],
                    space: destSpace,
                });
                iReckon(sir, beta_dest).asTo('beta exists in dest').isGonnaBeTruthy();
                if (beta_dest) {
                    const depGraph_beta_source = await getDependencyGraph({ ibGib: beta_v0_source, space: sourceSpace });
                    if (Object.keys(depGraph_alpha_v1Andcommon).length === 0) {
                        throw new Error(`(UNEXPECTED) depGraph_testRootAndV1Common empty? (E: 39b4d855ffa65476084b4123786da826)`);
                    }
                    const depGraph_beta_dest = await getDependencyGraph({ ibGib: beta_dest, space: destSpace });
                    const betaDepsAreEqual = graphsAreEquivalent({
                        graphA: depGraph_beta_source, graphB: depGraph_beta_dest
                    });
                    iReckon(sir, betaDepsAreEqual).asTo('beta got synced and graphs are equal').isGonnaBeTrue();
                }
            });
            await ifWe(sir, 'beta and deps synced', async () => {
                // beta's full dep graph should exist on dest
                const [beta_dest] = await getIbGibsFromCache_fallbackToSpaces({
                    addrs: [betaAddr_v0],
                    space: destSpace,
                });
                iReckon(sir, beta_dest).asTo('beta exists in dest').isGonnaBeTruthy();
                if (beta_dest) {
                    const depGraph_beta_source = await getDependencyGraph({ ibGib: beta_v0_source, space: sourceSpace });
                    if (Object.keys(depGraph_alpha_v1Andcommon).length === 0) {
                        throw new Error(`(UNEXPECTED) depGraph_testRootAndV1Common empty? (E: 39b4d855ffa65476084b4123786da826)`);
                    }
                    const depGraph_beta_dest = await getDependencyGraph({ ibGib: beta_dest, space: destSpace });
                    const betaDepsAreEqual = graphsAreEquivalent({
                        graphA: depGraph_beta_source,
                        graphB: depGraph_beta_dest,
                        slowButThorough: true,
                    });
                    iReckon(sir, betaDepsAreEqual).asTo('beta got synced and graphs are equal').isGonnaBeTrue();
                }
            });
        }
        catch (error) {
            console.error(`${lc} ${extractErrorMsg(error)}`);
            iReckon(sir, true).asTo('getKnowledgeMap errored out').isGonnaBeFalse();
        }
    });
});
//# sourceMappingURL=sync-conflict-basic-multitimelines.respec.mjs.map