import { beforeEach, describe, expect, it } from 'vitest';
import { Store, Trax, TraxObjectType, trax as trx } from '../index';
import { createTraxEnv } from '../core';
import { pause, Person, printEvents, SimpleFamilyStore } from './utils';

describe('Trax Core', () => {
    let trax: Trax;

    beforeEach(() => {
        trax = createTraxEnv();
    });

    function printLogs(minCycleId = 0, ignoreCycleEvents = true): string[] {
        return printEvents(trax.log, ignoreCycleEvents, minCycleId);
    }

    describe('Basics', () => {
        it('should support cycleComplete', async () => {
            expect(trax.pendingChanges).toBe(false);
            trax.log.info("A");
            expect(trax.pendingChanges).toBe(false);
            await trax.reconciliation();
            expect(trax.pendingChanges).toBe(false);

            expect(printLogs(0, false)).toMatchObject([
                '0:0 !CS - 0',
                '0:1 !LOG - A',
                '0:2 !CC - 0',
            ]);

            // no changes
            await trax.reconciliation();
            expect(trax.pendingChanges).toBe(false);
            expect(printLogs(0, false)).toMatchObject([
                '0:0 !CS - 0',
                '0:1 !LOG - A',
                '0:2 !CC - 0',
            ]);

            trax.log.info("B");
            await trax.reconciliation();
            expect(trax.pendingChanges).toBe(false);
            expect(printLogs(0, false)).toMatchObject([
                '0:0 !CS - 0',
                '0:1 !LOG - A',
                '0:2 !CC - 0',
                '1:0 !CS - 0',
                '1:1 !LOG - B',
                '1:2 !CC - 0',
            ]);

        });

        it('should discard non trax objects', async () => {
            expect(trax.isTraxObject(undefined)).toBe(false);
            expect(trax.isTraxObject(42)).toBe(false);
            expect(trax.isTraxObject({})).toBe(false);
            expect(trax.isTraxObject(true)).toBe(false);

            expect(trax.getTraxId(undefined)).toBe("");
            expect(trax.getTraxId(42)).toBe("");
            expect(trax.getTraxId({})).toBe("");
            expect(trax.getTraxId(true)).toBe("");

            expect(trax.getTraxObjectType(undefined)).toBe(TraxObjectType.NotATraxObject);
            expect(trax.getTraxObjectType(42)).toBe(TraxObjectType.NotATraxObject);
            expect(trax.getTraxObjectType({})).toBe(TraxObjectType.NotATraxObject);
            expect(trax.getTraxObjectType(true)).toBe(TraxObjectType.NotATraxObject);
        });
    });

    describe('Stores', () => {
        it('should be created wih a unique id', async () => {
            const initFn = (store: Store<any>) => {
                const root = store.init({ msg: "Hello" });
                return {
                    msg: root
                }
            }
            const s1 = trax.createStore("MyStore", initFn);
            expect(s1.id).toBe("MyStore");
            expect(trax.getTraxId(s1)).toBe(""); // the store wrapper is not a trax object
            const s2 = trax.createStore("MyStore", initFn);
            expect(s2.id).toBe("MyStore1");
            const s3 = trax.createStore("MyStore", initFn);
            expect(s3.id).toBe("MyStore2");

            s1.dispose();
            s2.dispose();
            s3.dispose();

            const s4 = trax.createStore("MyStore", initFn);
            expect(s4.id).toBe("MyStore"); // no need for suffix
            const s5 = trax.createStore("MyStore", initFn);
            expect(s5.id).toBe("MyStore1");

            s4.dispose();
            const s6 = trax.createStore("MyStore", initFn);
            expect(s6.id).toBe("MyStore"); // MyStore can be reused

            const s7 = trax.createStore(["MyStore", "A", 42], initFn);
            expect(s7.id).toBe("MyStore:A:42"); // id from array
        });

        it('should support creation with no init function', async () => {
            const ps = trax.createStore("PStore", {
                firstName: "Homer",
                lastName: "Simpson"
            });
            expect(ps.data.firstName).toBe("Homer");
        });

        it('should be able to define a custom dispose behaviour', async () => {
            let traces = "";
            const initFn = (store: Store<any>) => {
                const root = store.init({ msg: "Hello" });
                return {
                    msg: root,
                    dispose() {
                        traces += store.id + ";";
                    }
                }
            }
            const s1 = trax.createStore("MyStore", initFn);
            expect(s1.id).toBe("MyStore");
            const s2 = trax.createStore("MyStore", initFn);
            expect(s2.id).toBe("MyStore1");
            s1.dispose();
            expect(traces).toBe("MyStore;")
            s2.dispose();
            expect(traces).toBe("MyStore;MyStore1;")
            const s3 = trax.createStore("MyStore", initFn);
            expect(s3.id).toBe("MyStore");
            s3.dispose();
            expect(traces).toBe("MyStore;MyStore1;MyStore;")
        });

        it('should be identified as trax objects', async () => {
            const st = trax.createStore("MyStore", (store: Store<any>) => {
                store.init({ msg: "Hello" });
            });
            expect(trax.isTraxObject(st)).toBe(true);
        });

        it('should support init functions that don\'t return any object', async () => {
            const st = trax.createStore("MyStore", (store: Store<any>) => {
                store.init({ msg: "Hello" });
            });
            expect(st.id).toBe("MyStore");
            expect(typeof st.add).toBe("function");
            expect(trax.isTraxObject(st)).toBe(true);
            expect(trax.getTraxId(st)).toBe("MyStore");
        });

        it('should properly dispose all sub-objects', async () => {
            const ps = trax.createStore("PStore", {
                firstName: "Homer",
                lastName: "Simpson"
            });
            const o1 = ps.add("O1", { value: "o1" });
            const o2 = ps.add("O2", { value: "o2" });
            const rootId = trax.getTraxId(ps.data);
            const id1 = trax.getTraxId(o1);
            const id2 = trax.getTraxId(o2);
            expect(trax.getData(rootId)).toBe(ps.data);
            expect(trax.getData(id1)).toBe(o1);
            expect(trax.getData(id2)).toBe(o2);

            ps.dispose();
            expect(trax.getData(rootId)).toBe(undefined);
            expect(trax.getData(id1)).toBe(undefined);
            expect(trax.getData(id2)).toBe(undefined);
        });

        describe('Sub Stores', () => {

            it('should support create / dispose', async () => {
                const ps = trax.createStore("PStore", {
                    firstName: "Homer",
                    lastName: "Simpson"
                });

                const pss = ps.createStore("SubStore", (store: Store<{ msg: string }>) => {
                    const root = store.init({ msg: "" });

                    store.compute("Msg", () => {
                        root.msg = ps.data.firstName + "!";
                    });
                });
                const data = pss.data;

                const pssId = pss.id;
                expect(pssId).toBe("PStore>SubStore");
                expect(trax.getTraxId(pss)).toBe("PStore>SubStore");

                expect(pss.disposed).toBe(false);

                expect(data.msg).toBe("Homer!");

                await trax.reconciliation();
                ps.data.firstName = "Bart";
                await trax.reconciliation();
                expect(data.msg).toBe("Bart!");

                expect(trax.getStore(pssId)).toBe(pss);
                expect(pss.dispose()).toBe(true);
                expect(trax.getStore(pssId)).toBe(undefined);
                expect(pss.dispose()).toBe(false); // already disposed

                await trax.reconciliation();
                ps.data.firstName = "Lisa";
                await trax.reconciliation();
                expect(data.msg).toBe("Bart!"); // no changes
            });

            it('should be disposed when parent is disposed', async () => {
                const ps = trax.createStore("PStore", {
                    firstName: "Homer",
                    lastName: "Simpson"
                });

                const pss = ps.createStore("SubStore", (store: Store<{ msg: string }>) => {
                    const root = store.init({ msg: "" });

                    store.compute("Msg", () => {
                        root.msg = ps.data.firstName + "!";
                    });
                });
                const data1 = ps.data;
                const data1Id = trax.getTraxId(data1);
                expect(data1Id).toBe("PStore/data");

                const data2 = pss.data;
                const data2Id = trax.getTraxId(data2);
                expect(data1Id).toBe("PStore/data");
                expect(data2Id).toBe("PStore>SubStore/data");
                expect(trax.getData(data1Id)).toBe(data1);
                expect(trax.getData(data2Id)).toBe(data2);

                const psId = ps.id;
                const pssId = pss.id;
                const pr = pss.getProcessor("Msg")!;
                expect(pr.id).toBe("PStore>SubStore#Msg");
                expect(pr.disposed).toBe(false);

                expect(trax.getStore(psId)).toBe(ps);
                expect(trax.getStore(pssId)).toBe(pss);

                ps.dispose();
                expect(trax.getStore(psId)).toBe(undefined);
                expect(trax.getStore(pssId)).toBe(undefined);
                expect(trax.getData(data1Id)).toBe(undefined);
                expect(trax.getData(data2Id)).toBe(undefined);
                expect(ps.disposed).toBe(true);
                expect(pss.disposed).toBe(true);
                expect(pr.disposed).toBe(true);
            });

            it('should support substores in substores', async () => {
                let output = "";
                const ps = trax.createStore("PStore", {
                    firstName: "Homer",
                    lastName: "Simpson"
                });

                const pss = ps.createStore("SubStore", (store: Store<{ msg: string }>) => {
                    const root = store.init({ msg: "" });

                    store.compute("Msg", () => {
                        root.msg = ps.data.firstName + "!";
                    });

                    store.createStore("SubSubStore", (sst: Store<{ info: string }>) => {
                        const root2 = sst.init({ info: "" });

                        sst.compute("Info", () => {
                            output = root2.info = "<" + root.msg + ">";
                        });
                    })
                });

                expect(output).toBe("<Homer!>");

                await trax.reconciliation();
                ps.data.firstName = "Bart";

                await trax.reconciliation();
                expect(output).toBe("<Bart!>");

                const sst = pss.getStore("SubSubStore")!;
                expect(sst.id).toBe("PStore>SubStore>SubSubStore");
                expect(sst.disposed).toBe(false);
                expect(trax.getStore("PStore>SubStore>SubSubStore")).toBe(sst);

                sst.dispose();
                expect(sst.disposed).toBe(true);
                expect(trax.getStore("PStore>SubStore>SubSubStore")).toBe(undefined);
                expect(pss.getStore("SubSubStore")).toBe(undefined);

                await trax.reconciliation();
                ps.data.firstName = "Lisa";

                await trax.reconciliation();
                expect(output).toBe("<Bart!>"); // unchanged

                expect(pss.data.msg).toBe("Lisa!"); // not disposed
            });
        });

        describe('Wrappers', () => {
            function createPStore(throwError = false) {
                return trax.createStore("PStore", (store: Store<Person>) => {
                    const root = store.init({ firstName: "Homer", lastName: "Simpson" });

                    store.compute("PrettyName", () => {
                        root.prettyName = root.firstName + " " + root.lastName;
                    });

                    return {
                        person: root,
                        updateNameSync(value1: string, value2: string) {
                            root.firstName += value1;
                            if (throwError) {
                                throw "Something bad happened";
                            }
                            const r = root.lastName + value2 + value2;
                            root.lastName = r;
                            return r;
                        },
                        async updateNameAsync(value1: string, value2: string) {
                            root.firstName += value1;
                            await pause(1);
                            trax.log.event("@traxjs/trax/test/updateNameAsyncDone");
                            if (throwError) {
                                throw "Something bad happened";
                            }
                            root.lastName += value2 + value2;
                        },
                        updateNameAsync2: store.async(function* (value1: string, value2: string) {
                            root.firstName += value1;
                            yield pause(1);
                            trax.log.event("@traxjs/trax/test/updateNameAsync2Done");
                            if (throwError) {
                                throw "Something bad happened";
                            }
                            const r = root.lastName + value2 + value2;
                            root.lastName = r;
                            return r;
                        })
                    }
                });
            }

            it('should wrap sync functions and log calls', async () => {
                const ps = createPStore();
                expect(ps.person.firstName).toBe("Homer");

                await trax.reconciliation();
                trax.log.info("A")
                const r = ps.updateNameSync("A", "B");
                expect(r).toBe("SimpsonBB");
                trax.processChanges();
                expect(ps.person.prettyName).toBe("HomerA SimpsonBB");

                expect(printLogs(1)).toMatchObject([
                    "1:1 !LOG - A",
                    "1:2 !PCS - PStore.updateNameSync()",
                    "1:3 !GET - PStore/data.firstName -> 'Homer'",
                    "1:4 !SET - PStore/data.firstName = 'HomerA' (prev: 'Homer')",
                    "1:5 !DRT - PStore#PrettyName <- PStore/data.firstName",
                    "1:6 !GET - PStore/data.lastName -> 'Simpson'",
                    "1:7 !SET - PStore/data.lastName = 'SimpsonBB' (prev: 'Simpson')",
                    "1:8 !PCE - 1:2",
                    "1:9 !PCS - !Reconciliation #1 - 1 processor",
                    "1:10 !PCS - !Compute #2 (PStore#PrettyName) P1 Reconciliation - parentId=1:9",
                    "1:11 !GET - PStore/data.firstName -> 'HomerA'",
                    "1:12 !GET - PStore/data.lastName -> 'SimpsonBB'",
                    "1:13 !SET - PStore/data.prettyName = 'HomerA SimpsonBB' (prev: 'Homer Simpson')",
                    "1:14 !PCE - 1:10",
                    "1:15 !PCE - 1:9",
                    "1:16 !GET - PStore/data.prettyName -> 'HomerA SimpsonBB'",
                ]);
            });

            it('should log errors on sync function calls', async () => {
                const ps = createPStore(true);
                expect(ps.person.firstName).toBe("Homer");

                await trax.reconciliation();
                trax.log.info("A")
                ps.updateNameSync("A", "B");
                trax.processChanges();
                expect(ps.person.prettyName).toBe("HomerA Simpson"); // half processed

                expect(printLogs(1)).toMatchObject([
                    "1:1 !LOG - A",
                    "1:2 !PCS - PStore.updateNameSync()",
                    "1:3 !GET - PStore/data.firstName -> 'Homer'",
                    "1:4 !SET - PStore/data.firstName = 'HomerA' (prev: 'Homer')",
                    "1:5 !DRT - PStore#PrettyName <- PStore/data.firstName",
                    "1:6 !ERR - [TRAX] (PStore.updateNameSync) error: Something bad happened",
                    "1:7 !PCE - 1:2",
                    "1:8 !PCS - !Reconciliation #1 - 1 processor",
                    "1:9 !PCS - !Compute #2 (PStore#PrettyName) P1 Reconciliation - parentId=1:8",
                    "1:10 !GET - PStore/data.firstName -> 'HomerA'",
                    "1:11 !GET - PStore/data.lastName -> 'Simpson'",
                    "1:12 !SET - PStore/data.prettyName = 'HomerA Simpson' (prev: 'Homer Simpson')",
                    "1:13 !PCE - 1:9",
                    "1:14 !PCE - 1:8",
                    "1:15 !GET - PStore/data.prettyName -> 'HomerA Simpson'",
                ]);


            });

            it('should wrap async functions and log calls', async () => {
                const ps = createPStore();
                expect(ps.person.firstName).toBe("Homer");

                await trax.reconciliation();
                trax.log.info("A")
                ps.updateNameAsync("A", "B");
                await trax.log.awaitEvent("@traxjs/trax/test/updateNameAsyncDone");
                await trax.reconciliation();
                expect(ps.person.prettyName).toBe("HomerA SimpsonBB");

                expect(printLogs(1)).toMatchObject([
                    "1:1 !LOG - A",
                    "1:2 !PCS - PStore.updateNameAsync()",
                    "1:3 !GET - PStore/data.firstName -> 'Homer'",
                    "1:4 !SET - PStore/data.firstName = 'HomerA' (prev: 'Homer')",
                    "1:5 !DRT - PStore#PrettyName <- PStore/data.firstName",
                    "1:6 !PCE - 1:2",
                    "1:7 !PCS - !Reconciliation #1 - 1 processor",
                    "1:8 !PCS - !Compute #2 (PStore#PrettyName) P1 Reconciliation - parentId=1:7",
                    "1:9 !GET - PStore/data.firstName -> 'HomerA'",
                    "1:10 !GET - PStore/data.lastName -> 'Simpson'",
                    "1:11 !SET - PStore/data.prettyName = 'HomerA Simpson' (prev: 'Homer Simpson')",
                    "1:12 !PCE - 1:8",
                    "1:13 !PCE - 1:7",
                    "2:1 @traxjs/trax/test/updateNameAsyncDone - NO-DATA",
                    "2:2 !GET - PStore/data.lastName -> 'Simpson'",
                    "2:3 !SET - PStore/data.lastName = 'SimpsonBB' (prev: 'Simpson')",
                    "2:4 !DRT - PStore#PrettyName <- PStore/data.lastName",
                    "2:5 !PCS - !Reconciliation #2 - 1 processor",
                    "2:6 !PCS - !Compute #3 (PStore#PrettyName) P1 Reconciliation - parentId=2:5",
                    "2:7 !GET - PStore/data.firstName -> 'HomerA'",
                    "2:8 !GET - PStore/data.lastName -> 'SimpsonBB'",
                    "2:9 !SET - PStore/data.prettyName = 'HomerA SimpsonBB' (prev: 'HomerA Simpson')",
                    "2:10 !PCE - 2:6",
                    "2:11 !PCE - 2:5",
                    "3:1 !GET - PStore/data.prettyName -> 'HomerA SimpsonBB'",
                ]);
            });

            it('should log errors on async function calls', async () => {
                const ps = createPStore(true);
                expect(ps.person.firstName).toBe("Homer");

                await trax.reconciliation();
                trax.log.info("A")
                ps.updateNameAsync("A", "B");
                await trax.log.awaitEvent("@traxjs/trax/test/updateNameAsyncDone");
                await trax.reconciliation();
                expect(ps.person.prettyName).toBe("HomerA Simpson"); // half processed


                expect(printLogs(1)).toMatchObject([
                    "1:1 !LOG - A",
                    "1:2 !PCS - PStore.updateNameAsync()",
                    "1:3 !GET - PStore/data.firstName -> 'Homer'",
                    "1:4 !SET - PStore/data.firstName = 'HomerA' (prev: 'Homer')",
                    "1:5 !DRT - PStore#PrettyName <- PStore/data.firstName",
                    "1:6 !PCE - 1:2",
                    "1:7 !PCS - !Reconciliation #1 - 1 processor",
                    "1:8 !PCS - !Compute #2 (PStore#PrettyName) P1 Reconciliation - parentId=1:7",
                    "1:9 !GET - PStore/data.firstName -> 'HomerA'",
                    "1:10 !GET - PStore/data.lastName -> 'Simpson'",
                    "1:11 !SET - PStore/data.prettyName = 'HomerA Simpson' (prev: 'Homer Simpson')",
                    "1:12 !PCE - 1:8",
                    "1:13 !PCE - 1:7",
                    "2:1 @traxjs/trax/test/updateNameAsyncDone - NO-DATA",
                    "3:1 !ERR - [TRAX] (PStore.updateNameAsync) error: Something bad happened",
                    "4:1 !GET - PStore/data.prettyName -> 'HomerA Simpson'",
                ]);
            });

            it('should wrap generator functions and return async functions', async () => {
                const ps = createPStore();
                expect(ps.person.firstName).toBe("Homer");

                await trax.reconciliation();
                trax.log.info("A")
                const r = await ps.updateNameAsync2("A", "B");

                expect(r).toBe("SimpsonBB");
                expect(ps.person.prettyName).toBe("HomerA SimpsonBB");

                expect(printLogs(1)).toMatchObject([
                    "1:1 !LOG - A",
                    "1:2 !PCS - PStore.updateNameAsync2()",
                    "1:3 !GET - PStore/data.firstName -> 'Homer'",
                    "1:4 !SET - PStore/data.firstName = 'HomerA' (prev: 'Homer')",
                    "1:5 !DRT - PStore#PrettyName <- PStore/data.firstName",
                    "1:6 !PCP - 1:2",
                    "1:7 !PCS - !Reconciliation #1 - 1 processor",
                    "1:8 !PCS - !Compute #2 (PStore#PrettyName) P1 Reconciliation - parentId=1:7",
                    "1:9 !GET - PStore/data.firstName -> 'HomerA'",
                    "1:10 !GET - PStore/data.lastName -> 'Simpson'",
                    "1:11 !SET - PStore/data.prettyName = 'HomerA Simpson' (prev: 'Homer Simpson')",
                    "1:12 !PCE - 1:8",
                    "1:13 !PCE - 1:7",
                    "2:1 !PCR - 1:2",
                    "2:2 @traxjs/trax/test/updateNameAsync2Done - NO-DATA",
                    "2:3 !GET - PStore/data.lastName -> 'Simpson'",
                    "2:4 !SET - PStore/data.lastName = 'SimpsonBB' (prev: 'Simpson')",
                    "2:5 !DRT - PStore#PrettyName <- PStore/data.lastName",
                    "2:6 !PCE - 1:2",
                    "2:7 !PCS - !Reconciliation #2 - 1 processor",
                    "2:8 !PCS - !Compute #3 (PStore#PrettyName) P1 Reconciliation - parentId=2:7",
                    "2:9 !GET - PStore/data.firstName -> 'HomerA'",
                    "2:10 !GET - PStore/data.lastName -> 'SimpsonBB'",
                    "2:11 !SET - PStore/data.prettyName = 'HomerA SimpsonBB' (prev: 'HomerA Simpson')",
                    "2:12 !PCE - 2:8",
                    "2:13 !PCE - 2:7",
                    "3:1 !GET - PStore/data.prettyName -> 'HomerA SimpsonBB'",
                ]);
            });

            it('should provide means to support async init', async () => {
                const ps = trax.createStore("PStore", (store: Store<Person>) => {
                    const root = store.init({ firstName: "Homer", lastName: "Simpson" });

                    store.compute("PrettyName", () => {
                        root.prettyName = root.firstName + " " + root.lastName;
                    });

                    let initialized = false;

                    store.async("Init", function* () {
                        // allows to perform asyn operation to initialize the store
                        // e.g. call a server / read from local storage / etc.
                        yield pause(1);
                        initialized = true;
                        root.avatar = "AVATAR";
                        root.lastName += "!"; // will trigger PrettyName update
                        trax.log.event("@traxjs/trax/test/core/asyncInitDone");
                    })();

                    return {
                        person: root,
                        get initialized() {
                            return initialized;
                        },
                        updateNameSync(value1: string, value2: string) {
                            root.firstName += value1;
                            const r = root.lastName + value2 + value2;
                            root.lastName = r;
                            return r;
                        }
                    }
                });
                expect(ps.person.prettyName).toBe("Homer Simpson");
                expect(ps.person.avatar).toBe(undefined);
                expect(ps.initialized).toBe(false);

                await trax.log.awaitEvent("@traxjs/trax/test/core/asyncInitDone");
                await trax.reconciliation();
                expect(ps.person.prettyName).toBe("Homer Simpson!");
                expect(ps.person.avatar).toBe("AVATAR");
                expect(ps.initialized).toBe(true);

                expect(printLogs(0)).toMatchObject([
                    "0:1 !PCS - !StoreInit (PStore)",
                    "0:2 !NEW - S: PStore",
                    "0:3 !NEW - O: PStore/data",
                    "0:4 !NEW - P: PStore#PrettyName",
                    "0:5 !PCS - !Compute #1 (PStore#PrettyName) P1 Init - parentId=0:1",
                    "0:6 !GET - PStore/data.firstName -> 'Homer'",
                    "0:7 !GET - PStore/data.lastName -> 'Simpson'",
                    "0:8 !SET - PStore/data.prettyName = 'Homer Simpson' (prev: undefined)",
                    "0:9 !PCE - 0:5",
                    "0:10 !PCS - PStore.Init() - parentId=0:1",
                    "0:11 !PCP - 0:10",
                    "0:12 !PCE - 0:1",
                    "0:13 !GET - PStore/data.prettyName -> 'Homer Simpson'",
                    "0:14 !GET - PStore/data.avatar -> undefined",
                    "1:1 !PCR - 0:10",
                    "1:2 !SET - PStore/data.avatar = 'AVATAR' (prev: undefined)",
                    "1:3 !GET - PStore/data.lastName -> 'Simpson'",
                    "1:4 !SET - PStore/data.lastName = 'Simpson!' (prev: 'Simpson')",
                    "1:5 !DRT - PStore#PrettyName <- PStore/data.lastName",
                    "1:6 @traxjs/trax/test/core/asyncInitDone - NO-DATA",
                    "1:7 !PCE - 0:10",
                    "1:8 !PCS - !Reconciliation #1 - 1 processor",
                    "1:9 !PCS - !Compute #2 (PStore#PrettyName) P1 Reconciliation - parentId=1:8",
                    "1:10 !GET - PStore/data.firstName -> 'Homer'",
                    "1:11 !GET - PStore/data.lastName -> 'Simpson!'",
                    "1:12 !SET - PStore/data.prettyName = 'Homer Simpson!' (prev: 'Homer Simpson')",
                    "1:13 !PCE - 1:9",
                    "1:14 !PCE - 1:8",
                    "2:1 !GET - PStore/data.prettyName -> 'Homer Simpson!'",
                    "2:2 !GET - PStore/data.avatar -> 'AVATAR'",
                ]);
            });
        });

        describe('Errors', () => {

            it('must be raised when init is not called during the store initialization', async () => {
                const st = trax.createStore("MyStore", (store: Store<any>) => {
                    store.add("foo", { msg: "Hello" });
                });
                expect(printLogs()).toMatchObject([
                    '0:1 !PCS - !StoreInit (MyStore)',
                    "0:2 !NEW - S: MyStore",
                    '0:3 !NEW - O: MyStore/foo',
                    '0:4 !ERR - [TRAX] (MyStore) createStore init must define a root data object - see also: init()',
                    '0:5 !NEW - O: MyStore/data',
                    '0:6 !PCE - 0:1',
                ]);
            });

            it('must be raised if we try to remove the root object', async () => {
                const st = trax.createStore("MyStore", (store: Store<any>) => {
                    store.init({ msg: "Hello Ford" });
                });
                st.remove(st.data);
                expect(trax.isTraxObject(st.data)).toBe(true);
                expect(printLogs()).toMatchObject([
                    "0:1 !PCS - !StoreInit (MyStore)",
                    "0:2 !NEW - S: MyStore",
                    "0:3 !NEW - O: MyStore/data",
                    "0:4 !PCE - 0:1",
                    "0:5 !ERR - [TRAX] (MyStore/data) Root objects cannot be disposed through store.remove()",
                ]);
            });

            it('must be raised when init is called outside the init function', async () => {
                const st = trax.createStore("MyStore", (store: Store<any>) => {
                    store.init({ msg: "Hello" });
                });
                expect(printLogs()).toMatchObject([
                    '0:1 !PCS - !StoreInit (MyStore)',
                    "0:2 !NEW - S: MyStore",
                    '0:3 !NEW - O: MyStore/data',
                    '0:4 !PCE - 0:1',
                ]);
                st.init({ msg: "abc" });
                expect(printLogs()).toMatchObject([
                    '0:1 !PCS - !StoreInit (MyStore)',
                    "0:2 !NEW - S: MyStore",
                    '0:3 !NEW - O: MyStore/data',
                    '0:4 !PCE - 0:1',
                    '0:5 !ERR - [TRAX] (MyStore) Store.init can only be called during the store init phase',
                ]);
            });

            it('must be raised when init functions dont return an object', async () => {
                const st = trax.createStore("MyStore", (store: Store<any>) => {
                    store.init({ msg: "Hello" });
                    return 42;
                });
                expect(printLogs()).toMatchObject([
                    '0:1 !PCS - !StoreInit (MyStore)',
                    "0:2 !NEW - S: MyStore",
                    '0:3 !NEW - O: MyStore/data',
                    '0:4 !ERR - [TRAX] createStore init function must return a valid object (MyStore)',
                    '0:5 !PCE - 0:1',
                ]);
            });

            it('must be raised when init function throws an error', async () => {
                const st = trax.createStore("MyStore", (store: Store<any>) => {
                    store.init({ msg: "Hello" });
                    throw Error("Unexpected error");
                });
                expect(printLogs()).toMatchObject([
                    '0:1 !PCS - !StoreInit (MyStore)',
                    "0:2 !NEW - S: MyStore",
                    '0:3 !NEW - O: MyStore/data',
                    '0:4 !ERR - [TRAX] createStore init error (MyStore): Error: Unexpected error',
                    '0:5 !PCE - 0:1',
                ]);
            });

            it('must be raised when the store dispose throws an error', async () => {
                const st = trax.createStore("MyStore", (store: Store<any>) => {
                    store.init({ msg: "Hello" });
                    return {
                        dispose() {
                            throw Error("Unexpected dispose error");
                        }
                    }
                });
                st.dispose();
                expect(printLogs()).toMatchObject([
                    '0:1 !PCS - !StoreInit (MyStore)',
                    "0:2 !NEW - S: MyStore",
                    '0:3 !NEW - O: MyStore/data',
                    '0:4 !PCE - 0:1',
                    "0:5 !PCS - MyStore.dispose()",
                    "0:6 !ERR - [TRAX] (MyStore.dispose) error: Error: Unexpected dispose error",
                    "0:7 !PCE - 0:5",
                    "0:8 !DEL - MyStore/data",
                    "0:9 !DEL - MyStore",
                ]);
            });

            it('must be raised if store id is provided by the init function', async () => {
                const st = trax.createStore("MyStore", (store: Store<any>) => {
                    store.init({ msg: "Hello" });
                    return {
                        id: "abcd"
                    }
                });
                expect(printLogs()).toMatchObject([
                    '0:1 !PCS - !StoreInit (MyStore)',
                    "0:2 !NEW - S: MyStore",
                    '0:3 !NEW - O: MyStore/data',
                    '0:4 !ERR - [TRAX] Store id will be overridden and must not be provided by init function (MyStore)',
                    '0:5 !PCE - 0:1'
                ]);
            });

            it('must be raised in case of invalid id', async () => {
                const st = trax.createStore("My/Store/ABC", (store: Store<any>) => {
                    store.init({ msg: "Hello" });
                });
                expect(st.id).toBe("MyStoreABC");
                expect(printLogs()).toMatchObject([
                    '0:1 !ERR - [TRAX] Invalid trax id: My/Store/ABC (changed into MyStoreABC)',
                    '0:2 !PCS - !StoreInit (MyStoreABC)',
                    "0:3 !NEW - S: MyStoreABC",
                    '0:4 !NEW - O: MyStoreABC/data',
                    '0:5 !PCE - 0:2'
                ]);

                await trax.reconciliation();
                st.add("AB>CD", { foo: "bar" });
                expect(printLogs(1)).toMatchObject([
                    "1:1 !ERR - [TRAX] Invalid trax id: AB>CD (changed into ABCD)",
                    "1:2 !NEW - O: MyStoreABC/ABCD",
                ]);

                await trax.reconciliation();
                st.add("AB#C#D", { foo: "bar" });
                expect(printLogs(2)).toMatchObject([
                    "2:1 !ERR - [TRAX] Invalid trax id: AB#C#D (changed into ABCD)",
                ]);
            });

            it('must be raised in case of invalid add parameter', async () => {
                const st = trax.createStore("MyStore", (store: Store<any>) => {
                    store.init({ msg: "Hello" });
                    store.add("abc", 42);
                });
                expect(printLogs()).toMatchObject([
                    '0:1 !PCS - !StoreInit (MyStore)',
                    "0:2 !NEW - S: MyStore",
                    '0:3 !NEW - O: MyStore/data',
                    '0:4 !ERR - [TRAX] (MyStore) Store.add(abc): Invalid init object parameter: [number]',
                    '0:5 !NEW - O: MyStore/abc',
                    '0:6 !PCE - 0:1'
                ]);
            });

            it('must be raised if "data" is used as an id for a new object', async () => {
                const st = trax.createStore("MyStore", (store: Store<any>) => {
                    store.init({ msg: "Hello" });
                });
                const o = st.add("data", { msg: "abc" });
                const id = trax.getTraxId(o); // e.g. MyStore/87524
                expect(id.match(/^MyStore\/\d+$/)).not.toBe(null);

                expect(printLogs()).toMatchObject([
                    "0:1 !PCS - !StoreInit (MyStore)",
                    "0:2 !NEW - S: MyStore",
                    "0:3 !NEW - O: MyStore/data",
                    "0:4 !PCE - 0:1",
                    "0:5 !ERR - [TRAX] Store.add: Invalid id 'data' (reserved)",
                    "0:6 !NEW - O: " + id,
                ]);
            });

            it('must be raised if stores are used after being disposed', async () => {
                const ps = trax.createStore("PStore", {
                    firstName: "Homer",
                    lastName: "Simpson"
                });

                await trax.reconciliation();
                ps.dispose();
                const o = ps.add("Foo", { bar: 123 });

                expect(printLogs(1)).toMatchObject([
                    "1:1 !DEL - PStore/data",
                    "1:2 !DEL - PStore",
                    "1:3 !ERR - [TRAX] (PStore) Stores cannot be used after being disposed",
                ]);
            });

            it('must be raise if delete is called for SubStores', async () => {
                const ps = trax.createStore("PStore", {
                    firstName: "Homer",
                    lastName: "Simpson"
                });

                const pss = ps.createStore("SubStore", (store: Store<{ msg: string }>) => {
                    const root = store.init({ msg: "" });

                    store.compute("Msg", () => {
                        root.msg = ps.data.firstName + "!";
                    });
                });

                await trax.reconciliation();
                ps.remove(pss);

                expect(printLogs(1)).toMatchObject([
                    "1:1 !ERR - [TRAX] (PStore>SubStore) Stores cannot be disposed through store.remove()",
                ]);
            });

            it('must be raise if delete is called for Processors', async () => {
                const ps = trax.createStore("PStore", {
                    firstName: "Homer",
                    lastName: "Simpson",
                    misc: ""
                });

                const pr = ps.compute("Misc", () => {
                    const root = ps.data;
                    root.misc = root.firstName + " " + root.lastName;
                })

                await trax.reconciliation();
                ps.remove(pr);

                expect(printLogs(1)).toMatchObject([
                    "1:1 !ERR - [TRAX] (PStore#Misc) Processors cannot be disposed through store.remove()",
                ]);
            });
        });
    });

    describe('Reconciliation', () => {
        it('should support manual trigger (sync)', async () => {
            const st = trax.createStore("MyStore", (store: Store<Person>) => {
                const p = store.init({ firstName: "Homer", lastName: "Simpson" });
                store.compute("PrettyName", () => {
                    const nm = p.firstName + " " + p.lastName;
                    p.prettyName = nm;
                    p.prettyNameLength = nm.length;
                });
            });

            trax.log.info("A");
            expect(trax.pendingChanges).toBe(false);
            trax.processChanges(); // no effect (no changes)
            trax.log.info("B");

            st.data.lastName = "SIMPSON";
            trax.log.info("C");
            expect(trax.pendingChanges).toBe(true);
            trax.processChanges();
            expect(trax.pendingChanges).toBe(false);
            trax.log.info("D");
            trax.processChanges(); // no effect (no changes)

            await trax.reconciliation();

            expect(printLogs(0, false)).toMatchObject([
                "0:0 !CS - 0",
                "0:1 !PCS - !StoreInit (MyStore)",
                "0:2 !NEW - S: MyStore",
                "0:3 !NEW - O: MyStore/data",
                "0:4 !NEW - P: MyStore#PrettyName",
                "0:5 !PCS - !Compute #1 (MyStore#PrettyName) P1 Init - parentId=0:1",
                "0:6 !GET - MyStore/data.firstName -> 'Homer'",
                "0:7 !GET - MyStore/data.lastName -> 'Simpson'",
                "0:8 !SET - MyStore/data.prettyName = 'Homer Simpson' (prev: undefined)",
                "0:9 !SET - MyStore/data.prettyNameLength = 13 (prev: undefined)",
                "0:10 !PCE - 0:5",
                "0:11 !PCE - 0:1",
                "0:12 !LOG - A",
                "0:13 !LOG - B",
                "0:14 !SET - MyStore/data.lastName = 'SIMPSON' (prev: 'Simpson')",
                "0:15 !DRT - MyStore#PrettyName <- MyStore/data.lastName",
                "0:16 !LOG - C",
                "0:17 !PCS - !Reconciliation #1 - 1 processor",
                "0:18 !PCS - !Compute #2 (MyStore#PrettyName) P1 Reconciliation - parentId=0:17",
                "0:19 !GET - MyStore/data.firstName -> 'Homer'",
                "0:20 !GET - MyStore/data.lastName -> 'SIMPSON'",
                "0:21 !SET - MyStore/data.prettyName = 'Homer SIMPSON' (prev: 'Homer Simpson')",
                "0:22 !PCE - 0:18",
                "0:23 !PCE - 0:17",
                "0:24 !LOG - D",
                "0:25 !CC - 0",
            ]);

        });

        it('should support automatic trigger (async)', async () => {
            const st = trax.createStore("MyStore", (store: Store<Person>) => {
                const p = store.init({ firstName: "Homer", lastName: "Simpson" });
                store.compute("PrettyName", () => {
                    const nm = p.firstName + " " + p.lastName;
                    p.prettyName = nm;
                    p.prettyNameLength = nm.length;
                });
            });

            const p = st.data;

            trax.log.info("A");
            expect(trax.pendingChanges).toBe(false);
            p.lastName = "SIMPSON";
            expect(trax.pendingChanges).toBe(true);

            trax.log.info("B");
            await trax.reconciliation();
            trax.log.info("C");

            expect(trax.pendingChanges).toBe(false);
            p.firstName = "Bart";
            expect(trax.pendingChanges).toBe(true);
            p.lastName = "Simpson";

            await trax.reconciliation();
            expect(p.prettyName).toBe("Bart Simpson");


            expect(printLogs(0, false)).toMatchObject([
                "0:0 !CS - 0",
                "0:1 !PCS - !StoreInit (MyStore)",
                "0:2 !NEW - S: MyStore",
                "0:3 !NEW - O: MyStore/data",
                "0:4 !NEW - P: MyStore#PrettyName",
                "0:5 !PCS - !Compute #1 (MyStore#PrettyName) P1 Init - parentId=0:1",
                "0:6 !GET - MyStore/data.firstName -> 'Homer'",
                "0:7 !GET - MyStore/data.lastName -> 'Simpson'",
                "0:8 !SET - MyStore/data.prettyName = 'Homer Simpson' (prev: undefined)",
                "0:9 !SET - MyStore/data.prettyNameLength = 13 (prev: undefined)",
                "0:10 !PCE - 0:5",
                "0:11 !PCE - 0:1",
                "0:12 !LOG - A",
                "0:13 !SET - MyStore/data.lastName = 'SIMPSON' (prev: 'Simpson')",
                "0:14 !DRT - MyStore#PrettyName <- MyStore/data.lastName",
                "0:15 !LOG - B",
                "0:16 !PCS - !Reconciliation #1 - 1 processor",
                "0:17 !PCS - !Compute #2 (MyStore#PrettyName) P1 Reconciliation - parentId=0:16",
                "0:18 !GET - MyStore/data.firstName -> 'Homer'",
                "0:19 !GET - MyStore/data.lastName -> 'SIMPSON'",
                "0:20 !SET - MyStore/data.prettyName = 'Homer SIMPSON' (prev: 'Homer Simpson')",
                "0:21 !PCE - 0:17",
                "0:22 !PCE - 0:16",
                "0:23 !CC - 0",
                "1:0 !CS - 0",
                "1:1 !LOG - C",
                "1:2 !SET - MyStore/data.firstName = 'Bart' (prev: 'Homer')",
                "1:3 !DRT - MyStore#PrettyName <- MyStore/data.firstName",
                "1:4 !SET - MyStore/data.lastName = 'Simpson' (prev: 'SIMPSON')",
                "1:5 !PCS - !Reconciliation #2 - 1 processor",
                "1:6 !PCS - !Compute #3 (MyStore#PrettyName) P1 Reconciliation - parentId=1:5",
                "1:7 !GET - MyStore/data.firstName -> 'Bart'",
                "1:8 !GET - MyStore/data.lastName -> 'Simpson'",
                "1:9 !SET - MyStore/data.prettyName = 'Bart Simpson' (prev: 'Homer SIMPSON')",
                "1:10 !SET - MyStore/data.prettyNameLength = 12 (prev: 13)",
                "1:11 !PCE - 1:6",
                "1:12 !PCE - 1:5",
                "1:13 !CC - 0",
                "2:0 !CS - 0",
                "2:1 !GET - MyStore/data.prettyName -> 'Bart Simpson'",
            ]);

        });
    });

    describe('Main trax', () => {
        it('should be accessible from index.ts', async () => {
            expect(typeof trx.createStore).toBe("function");
        });
    });
});
