import { beforeEach, describe, expect, it } from 'vitest';
import { Store, Trax } from '../types';
import { createTraxEnv, tmd } from '../core';
import { SimpleFamilyStore, printEvents } from './utils';


describe('Trax Objects', () => {
    let trax: Trax, fst: Store<SimpleFamilyStore>;

    beforeEach(() => {
        trax = createTraxEnv();
        fst = trax.createStore("SimpleFamilyStore", (store: Store<SimpleFamilyStore>) => {
            store.init({
                childNames: "",
                father: {
                    firstName: "Homer",
                    lastName: ""
                }
            });
        });
    });

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

    describe('Basics', () => {
        it('must get the root object and wrap sub-objects', async () => {
            const r1 = fst.get<SimpleFamilyStore>("data")!;
            expect(r1.father!.firstName).toBe("Homer");

            const r2 = fst.get<SimpleFamilyStore>("data")!;
            expect(r1).toBe(r2);
            expect(r2.childNames).toBe("");

            expect(trax.getTraxId(r1)).toBe("SimpleFamilyStore/data");

            expect(printLogs(true, 0)).toMatchObject([
                '0:1 !PCS - !StoreInit (SimpleFamilyStore)',
                '0:2 !NEW - S: SimpleFamilyStore',
                '0:3 !NEW - O: SimpleFamilyStore/data',
                '0:4 !PCE - 0:1',
                // Note: the following logs are on cycle 1 because the store was initialised in beforeEach
                "1:1 !NEW - O: SimpleFamilyStore/data*father",
                "1:2 !GET - SimpleFamilyStore/data.father -> '[TRAX SimpleFamilyStore/data*father]'",
                "1:3 !GET - SimpleFamilyStore/data*father.firstName -> 'Homer'",
                "1:4 !GET - SimpleFamilyStore/data.childNames -> ''",
            ]);
        });

        it('must support object wrapping', async () => {
            let o1 = fst.add("foo", { foo: "bar" });

            expect(trax.isTraxObject(o1)).toBe(true);
            expect(trax.isTraxObject({ foo: "bar" })).toBe(false);
            expect(trax.getTraxId(o1)).toBe("SimpleFamilyStore/foo");

            let o2 = fst.add("bar", { foo: "bar2" });
            expect(trax.isTraxObject(o2)).toBe(true);
            expect(trax.getTraxId(o2)).toBe("SimpleFamilyStore/bar");

            expect(printLogs()).toMatchObject([
                '1:1 !NEW - O: SimpleFamilyStore/foo',
                '1:2 !NEW - O: SimpleFamilyStore/bar',
            ]);
        });

        it('must support using other trax objects to build advance ids (same store)', async () => {
            let o1 = fst.add([fst.data, "foo"], { foo: "bar" });
            expect(trax.getTraxId(o1)).toBe("SimpleFamilyStore/data:foo");

            // this will generate an error
            let o2 = fst.add([{ blah: "blah" }, "foo"], { foo: "bar" });
            const id = trax.getTraxId(o2); // e.g. SimpleFamilyStore/17686:foo
            const m = id.match(/^SimpleFamilyStore\/\d+\:foo$/);
            expect(m).not.toBe(null);

            expect(printLogs()).toMatchObject([
                "1:1 !NEW - O: SimpleFamilyStore/data:foo",
                "1:2 !ERR - [TRAX] Invalid id param: not a trax object",
                "1:3 !NEW - O: " + id
            ]);
        });

        it('must support using other trax objects to build advance ids (different store)', async () => {
            const st = trax.createStore("AnotherStore", (store: Store<{ msg: string }>) => {
                store.init({ msg: "Hello World" })
            })

            let o = fst.add([st.data, "foo"], { foo: "bar" });
            expect(trax.getTraxId(o)).toBe("SimpleFamilyStore/AnotherStore-data:foo");

            expect(printLogs()).toMatchObject([
                "1:1 !PCS - !StoreInit (AnotherStore)",
                "1:2 !NEW - S: AnotherStore",
                "1:3 !NEW - O: AnotherStore/data",
                "1:4 !PCE - 1:1",
                "1:5 !NEW - O: SimpleFamilyStore/AnotherStore-data:foo",
            ]);
        });

        it('must return the same object and ignore new default values', async () => {
            let o1 = fst.add("foo", { foo: "bar" });
            let o2 = fst.add("foo", { blah: "baz" });
            let o3 = fst.get("foo");

            expect(o2).toBe(o1);
            expect(o3).toBe(o1);
            expect(o2.blah).toBe(undefined);
            expect((o2 as any).foo).toBe("bar");
        });

        it('must support delete and create new objects if previous id was deleted', async () => {
            let o1 = fst.add("foo", { foo: "bar" });

            const md1a = tmd(o1)!;
            expect(md1a.id).toBe("SimpleFamilyStore/foo");
            let dr = fst.remove(o1);
            expect(dr).toBe(true);
            const md1b = tmd(o1);
            expect(md1b).toBe(undefined);
            dr = fst.remove(o1);
            expect(dr).toBe(false);

            let o2 = fst.add(["foo", "bar"], { foo: "bar" });
            expect(o2 !== o1).toBe(true);

            dr = fst.remove({});
            expect(dr).toBe(false);

            dr = fst.remove(["foo", "bar"]);
            expect(dr).toBe(false);
            dr = fst.remove(o2);
            expect(dr).toBe(true);

            let o3 = fst.add("x", { foo: 123 });
            dr = fst.remove(o3);
            expect(dr).toBe(true);
            fst.add("x", { foo: 123 });
            dr = fst.remove(o3);
            expect(dr).toBe(false);

            expect(printLogs()).toMatchObject([
                "1:1 !NEW - O: SimpleFamilyStore/foo",
                "1:2 !DEL - SimpleFamilyStore/foo",
                "1:3 !NEW - O: SimpleFamilyStore/foo:bar",
                "1:4 !DEL - SimpleFamilyStore/foo:bar",
                "1:5 !NEW - O: SimpleFamilyStore/x",
                "1:6 !DEL - SimpleFamilyStore/x",
                "1:7 !NEW - O: SimpleFamilyStore/x",
            ]);
        });

        it('must support JSON.stringify and not expose any specific property', async () => {
            let o = fst.add("foo", { foo: "bar" });
            expect(JSON.stringify(o)).toBe('{"foo":"bar"}');
            expect(tmd(o)!.id).toBe("SimpleFamilyStore/foo");
        });

        it('must not re-wrap deleted data objects', async () => {
            let f = fst.add("ford", { name: { firstName: "Ford", lastName: "Prefect" }, planet: "Betelgeuse V" });
            expect(f.name.firstName).toBe("Ford");
            fst.remove(f.name);

            trax.log.info("A");
            f.name.firstName = "FORD"
            expect(f.name.firstName).toBe("FORD");

            expect(printLogs()).toMatchObject([
                "1:1 !NEW - O: SimpleFamilyStore/ford",
                "1:2 !NEW - O: SimpleFamilyStore/ford*name",
                "1:3 !GET - SimpleFamilyStore/ford.name -> '[TRAX SimpleFamilyStore/ford*name]'",
                "1:4 !GET - SimpleFamilyStore/ford*name.firstName -> 'Ford'",
                "1:5 !GET - SimpleFamilyStore/ford.name -> '[TRAX SimpleFamilyStore/ford*name]'",
                "1:6 !DEL - SimpleFamilyStore/ford*name",
                "1:7 !LOG - A",
                "1:8 !GET - SimpleFamilyStore/ford.name -> {\"firstName\":\"Ford\",\"lastName\":\"Prefect\"}",
                "1:9 !GET - SimpleFamilyStore/ford.name -> {\"firstName\":\"FORD\",\"lastName\":\"Prefect\"}",
            ]);
        });

        it('must log GET and SET until object is deleted', async () => {
            let p = fst.add("HS", { firstName: "Homer", lastName: "Simpson" });

            let name1 = p.firstName + " " + p.lastName;
            expect(name1).toBe("Homer Simpson");

            p.lastName = "SIMPSON";
            fst.remove(p);

            let name2 = p.firstName + " " + p.lastName;
            expect(name2).toBe("Homer SIMPSON");

            p.lastName = "Simpson";
            expect(p.lastName).toBe("Simpson");

            trax.log.info("END");

            expect(printLogs()).toMatchObject([
                '1:1 !NEW - O: SimpleFamilyStore/HS',
                '1:2 !GET - SimpleFamilyStore/HS.firstName -> \'Homer\'',
                '1:3 !GET - SimpleFamilyStore/HS.lastName -> \'Simpson\'',
                '1:4 !SET - SimpleFamilyStore/HS.lastName = \'SIMPSON\' (prev: \'Simpson\')',
                '1:5 !DEL - SimpleFamilyStore/HS',
                '1:6 !LOG - END'
            ]);
        });
    });

});
