'use strict';

import * as chai from 'chai';
import {internalSymbol} from "../../../source/constants.mjs";
import {customElementUpdaterLinkSymbol} from "../../../source/dom/constants.mjs";
import {getDocument} from "../../../source/dom/util.mjs";
import {ProxyObserver} from "../../../source/types/proxyobserver.mjs";
import {chaiDom} from "../../util/chai-dom.mjs";
import {initJSDOM} from "../../util/jsdom.mjs";


let expect = chai.expect;
chai.use(chaiDom);

let html1 = `
    <div id="test1">
    </div>
`;

let html2 = `
    <input data-monster-bind="path:a" id="test2" data-monster-attributes="value path:a">
`;

// defined in constants.mjs
const updaterSymbolKey = "@schukai/monster/dom/custom-element@@options-updater-link"
const updaterSymbolSymbol = Symbol.for(updaterSymbolKey);


describe('DOM', function () {

    let CustomElement, registerCustomElement, TestComponent, document, TestComponent2, TestStateComponent, TestMutationComponent, HiddenStateClass, assignUpdaterToElement,
        addObjectWithUpdaterToElement;

    describe("assignUpdaterToElement", function () {

        before(function (done) {
            const options = {};
            initJSDOM(options).then(() => {
                import("../../../source/dom/updater.mjs").then((yy) => {
                    addObjectWithUpdaterToElement = yy['addObjectWithUpdaterToElement'];
                    import("../../../source/dom/customelement.mjs").then((m) => {
                        try {
                            CustomElement = m['CustomElement'];
                            assignUpdaterToElement = function (elements, object) {
                                return addObjectWithUpdaterToElement.call(this, elements, updaterSymbolSymbol, object);
                            }
                            document = getDocument();

                            done()
                        } catch (e) {
                            done(e);
                        }


                    }).catch((e) => {
                        done(e);
                    });

                }).catch((e) => {
                    done(e);
                });
            });
        })

        beforeEach(() => {
            let mocks = document.getElementById('mocks');
            mocks.innerHTML = html2;
        })

        afterEach(() => {
            let mocks = document.getElementById('mocks');
            mocks.innerHTML = "";
        })

        /**
         * this test try to simulate the bug that was found in the assignUpdaterToElement function.
         * The bug was that the updater was not assigned to the element when the element was created.
         *
         * unfortunately, this test does not reproduce the bug.
         */
        it("should assign an updater to an element", function (done) {
            let element = document.getElementById('test2');

            expect(document.getElementById("mocks").innerHTML).to.equal(html2);

            const a = {a: 1};
            const b = {b: 2};

            const ap = new ProxyObserver(a);
            const bp = new ProxyObserver(b);

            const x = ap.getSubject()
            const y = bp.getSubject()

            const set = new Set();
            set.add(element);

            assignUpdaterToElement.call(element, set, ap);
            assignUpdaterToElement.call(element, set, bp);

            expect(JSON.stringify(x)).to.equal('{"a":1}');
            expect(JSON.stringify(y)).to.equal('{"b":2}');

            const sy = updaterSymbolSymbol;

            let v = element.getAttribute("data-monster-objectlink");
            expect(v).to.equal('Symbol(' + updaterSymbolKey + ')');

            const updater = element[sy];

            for (const v of updater) {
                for (const u of v) {
                    u.run().then(() => {
                        u.enableEventProcessing();
                    });
                }
            }

            expect(updater).to.be.an.instanceof(Set);
            expect(updater).to.be.a("Set");

            x.a = 3;
            bp.getSubject().b = 4;

            setTimeout(() => {

                let mockHTML = document.getElementById("mocks");

                // html expexted:
                // <input data-monster-bind="path:a" id="test2" data-monster-attributes="value path:a" data-monster-objectlink="Symbol(@schukai/monster/dom/@@object-updater-link)" value="3">

                expect(mockHTML.querySelector("#test2")).to.have.value('3')
                expect(mockHTML.querySelector("#test2")).to.have.attribute('data-monster-objectlink', 'Symbol(' + updaterSymbolKey + ')')
                //expect(mockHTML).to.have.html(resultHTML);

                expect(element.value).to.equal("3");

                expect(JSON.stringify(ap.getRealSubject())).to.equal('{"a":3}');
                expect(JSON.stringify(bp.getRealSubject())).to.equal('{"b":4}');
                done()
            }, 50)

        })

    })

    describe('CustomElement()', function () {

        before(function (done) {
            initJSDOM({}).then(() => {

                import("../../../source/dom/customelement.mjs").then((m) => {

                    try {
                        CustomElement = m['CustomElement'];
                        registerCustomElement = m['registerCustomElement'];
                        TestComponent = class extends CustomElement {
                            static getTag() {
                                return "monster-testclass"
                            }
                        }

                        registerCustomElement(TestComponent)

                        TestComponent2 = class extends CustomElement {
                            static getTag() {
                                return "monster-testclass2"
                            }

                            /**
                             *
                             * @return {Object}
                             */
                            get defaults() {

                                return Object.assign({}, super.defaults, {
                                    demotest: undefined,
                                    templates: {
                                        main: '<h1></h1><article><p>test</p><div id="container"></div></article>'
                                    },
                                })
                            }

                        }

                        registerCustomElement(TestComponent2)

                        const hiddenKey = Symbol("hidden");
                        class HiddenState {
                            constructor(presentation) {
                                this[hiddenKey] = presentation;
                            }

                            get presentation() {
                                return this[hiddenKey];
                            }

                            getClone() {
                                return new HiddenState(this.presentation);
                            }
                        }

                        HiddenStateClass = HiddenState;

	                        TestStateComponent = class extends CustomElement {
                            static getTag() {
                                return "monster-testclass-state"
                            }

                            get defaults() {
                                return Object.assign({}, super.defaults, {
                                    current: new HiddenStateClass(""),
                                    templates: {
                                        main: '<div id="state" data-monster-replace="path:current.presentation"></div>'
                                    }
                                })
                            }
                        }

	                        registerCustomElement(TestStateComponent)

	                        TestMutationComponent = class extends CustomElement {
	                            static getTag() {
	                                return "monster-testclass-mutation"
	                            }

	                            get defaults() {
	                                return Object.assign({}, super.defaults, {
	                                    features: {
	                                        mutationObserver: true,
	                                    },
	                                    templates: {
	                                        main: '<div id="mutation-root"><span data-monster-replace="static:ok"></span></div>'
	                                    }
	                                })
	                            }
	                        }

	                        registerCustomElement(TestMutationComponent)

	                        document = getDocument();
                        done()
                    } catch (e) {
                        done(e);
                    }


                });

            });
        })

        beforeEach(() => {
            let mocks = document.getElementById('mocks');
            mocks.innerHTML = html1;
        })

        afterEach(() => {
            let mocks = document.getElementById('mocks');
            mocks.innerHTML = "";
        })

        describe('CustomElement() with Config', function () {
            it('should read config from tag', function () {

                let mocks = document.getElementById('mocks');
                mocks.innerHTML = `
                
                <script id="config1" type="application/json">
                {
                    "demotest":1425
                }
                </script>
                
                <monster-testclass2 id="thisisatest" data-monster-options-selector="#config1">
                </monster-testclass2>
                `;

                let monster = document.getElementById('thisisatest');
                expect(monster.getOption('demotest')).is.eql(1425);

            });
        });

        describe('create', function () {
            it('should return custom-element object', function () {
                let d = new TestComponent();
                expect(typeof d).is.equal('object');
            });
        });

        describe('connect empty element', function () {
            it('document should contain monster-testclass', function () {
                let d = document.createElement('monster-testclass');
                document.getElementById('test1').appendChild(d);
                expect(document.getElementsByTagName('monster-testclass').length).is.equal(1);
                // no data-monster-objectlink="Symbol(monsterUpdater)" because it has nothing to update
                // but data-monster-error="Error: html is not set."
                expect(document.getElementById('test1')).contain.html('<monster-testclass data-monster-error="html is not set."></monster-testclass>');
            });
        });

        describe('connect element with html', function () {
            it('document should contain monster-testclass2', function (done) {
                let d = document.createElement('monster-testclass2');
                document.getElementById('test1').appendChild(d);

                // insert DOM run in extra process via setTimeout!
                setTimeout(function () {
                    try {
                        expect(document.getElementsByTagName('monster-testclass2').length).is.equal(1);
                        expect(document.getElementsByTagName('monster-testclass2').item(0).shadowRoot.innerHTML).is.equal('<h1></h1><article><p>test</p><div id="container"></div></article>');
                        expect(document.getElementById('test1')).contain.html('<monster-testclass2 data-monster-objectlink="Symbol(' + updaterSymbolKey + ')"></monster-testclass2>');
                        return done();
                    } catch (e) {
                        done(e);
                    }

                }, 10);

            });
        });

        describe('shadow root lifecycle', function () {
            it('should support closed shadow roots without adding an error', function (done) {
                const htmlTAG = 'monster-testclass-closed';

                class ClosedShadowComponent extends CustomElement {
                    static getTag() {
                        return htmlTAG;
                    }

                    get defaults() {
                        return Object.assign({}, super.defaults, {
                            shadowMode: 'closed',
                            templates: {
                                main: '<span id="inside">ok</span>'
                            },
                        });
                    }
                }

                registerCustomElement(ClosedShadowComponent);

                let d = document.createElement(htmlTAG);
                document.getElementById('test1').appendChild(d);

                setTimeout(function () {
                    try {
                        expect(d.hasAttribute('data-monster-error')).to.be.false;
                        expect(d.shadowRoot).to.equal(null);
                        done();
                    } catch (e) {
                        done(e);
                    }
                }, 10);
            });
        });

        describe('attribute lifecycle', function () {
            it('should not double-call attributeChangedCallback for observed attributes', function (done) {
                const htmlTAG = 'monster-testclass-observed';

                class ObservedAttributeComponent extends CustomElement {
                    static getTag() {
                        return htmlTAG;
                    }

                    static get observedAttributes() {
                        return ['data-demo'];
                    }

                    get defaults() {
                        return Object.assign({}, super.defaults, {
                            shadowMode: false,
                        });
                    }

                    constructor() {
                        super();
                        this.attributeChangedCalls = [];
                    }

                    attributeChangedCallback(attrName, oldVal, newVal) {
                        this.attributeChangedCalls.push([attrName, oldVal, newVal]);
                        super.attributeChangedCallback(attrName, oldVal, newVal);
                    }
                }

                registerCustomElement(ObservedAttributeComponent);

                let d = document.createElement(htmlTAG);
                document.getElementById('test1').appendChild(d);
                d.setAttribute('data-demo', '1');

                setTimeout(function () {
                    try {
                        expect(d.attributeChangedCalls).to.have.length(1);
                        done();
                    } catch (e) {
                        done(e);
                    }
                }, 10);
            });
        });

        describe('Options change', function () {

            it('delegatesFocus should change from true to false', function () {
                let element = document.createElement('monster-testclass')

                const o = element[internalSymbol].realSubject;
                expect(Object.is(element[internalSymbol].realSubject, o)).to.be.true;

                expect(element[internalSymbol].realSubject.options.delegatesFocus).to.be.true;
                expect(element[internalSymbol].subject.options.delegatesFocus).to.be.true;
                expect(element.getOption('delegatesFocus')).to.be.true;
                expect(Object.is(element[internalSymbol].realSubject, o)).to.be.true;

                // element.setAttribute(ATTRIBUTE_OPTIONS, JSON.stringify({delegatesFocus: false}));
                // expect(Object.is(element[internalSymbol].realSubject, o)).to.be.true;
                //
                // expect(element.getOption('delegatesFocus')).to.be.false;
                // expect(element[internalSymbol].realSubject.options.delegatesFocus).to.be.false;
                // expect(Object.is(element[internalSymbol].realSubject, o)).to.be.true;

            })


        })

        describe('Updater sync for instance values', function () {
            it('should keep class instances and update presentation', function (done) {
                let d = document.createElement('monster-testclass-state');
                document.getElementById('test1').appendChild(d);

                setTimeout(function () {
                    d.setOption('current', new HiddenStateClass('OK'));

                    setTimeout(function () {
                        try {
                            expect(d.shadowRoot.querySelector('#state').textContent).to.equal('OK');
                            done();
                        } catch (e) {
                            done(e);
                        }
                    }, 20);
                }, 10);
            });
        })

        describe('setOptions()', function () {
            [
                ['shadowMode', 'x1'],
                ['templates.main', 'x2'], // is explicitly set to undefined
                ['delegatesFocus', 'x4'],
            ].forEach(function (data) {


                let key = data.shift()
                let newValue = data.shift()

                let text = key + ' should return ' + newValue;
                if (newValue !== undefined) {
                    text = key + ' was not set, therefore default ' + newValue;
                }


                it(text, function () {

                    let d = document.createElement('monster-testclass');
                    expect(d.getOption(key)).to.be.not.equal(newValue);
                    let x = d.setOption(key, newValue);
                    expect(d.getOption(key)).to.be.equal(newValue);
                })


            })
        });

        describe('getOptions()', function () {

            [
                ['shadowMode', 'open'],
                ['templates.main', undefined], // is explicitly set to undefined
                ['delegatesFocus', true],
                ['x.y.z', true, true], // x.y.z isnt set, defaultValue is used
                ['x', true, true] // x isnt set, defaultValue is used
            ].forEach(function (data) {


                let key = data.shift()
                let value = data.shift()
                let defaultValue = data.shift()

                let text = key + ' should return ' + value;
                if (defaultValue !== undefined) {
                    text = key + ' was not set, therefore default ' + defaultValue;
                }


                it(text, function () {

                    let d = document.createElement('monster-testclass');
                    let x = d.getOption(key, defaultValue);
                    expect(x).to.be.equal(value);
                })


            })
        })

        /**
         * @link https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/113
         */
        describe('Assign CSSStyle as Array with wrong type', function () {

            const htmlTAG = 'monster-testclass-x1';

            let mocks, TestComponentX1;
            beforeEach(() => {

                mocks = document.getElementById('mocks');
                mocks.innerHTML = html1;


                TestComponentX1 = class extends CustomElement {
                    static getTag() {
                        return htmlTAG
                    }

                    static getCSSStyleSheet() {
                        return [true];
                    }

                    /**
                     * @return {Object}
                     */
                    get defaults() {

                        return Object.assign({}, super.defaults, {
                            templates: {
                                main: '<h1>test</h1>'
                            },
                        })
                    }

                }

                registerCustomElement(TestComponentX1)


            })

            it(htmlTAG + " should throw Exception", function (done) {
                let d = document.createElement(htmlTAG);

                let div = document.getElementById('test1');
                div.append(d);


                const cssName = CSSStyleSheet?.name || "CSSStyleSheet";
                expect(div).contain.html(
                    `data-monster-error="value is not an instance of ${cssName}"`,
                );
                done();

            })


        })


        /**
         * @link https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/113
         */
        describe('Assign CSSStyle as Array and CSSStylesheet', function () {

            const htmlTAG = 'monster-testclass-x113-2';

            let mocks, TestComponentX113X2;
            beforeEach(() => {

                mocks = document.getElementById('mocks');
                mocks.innerHTML = html1;

                TestComponentX113X2 = class extends CustomElement {
                    static getTag() {
                        return htmlTAG
                    }

                    /**
                     * @return {Object}
                     */
                    get defaults() {
                        return Object.assign({}, super.defaults, {
                            templates: {main: '<h1>test</h1>'},
                        })
                    }


                    static getCSSStyleSheet() {

                        const s = (new CSSStyleSheet())
                        s.insertRule('a { color : red}');

                        return [s];
                    }
                }

                registerCustomElement(TestComponentX113X2)


            })

            it(htmlTAG + " should throw Exception 2", function (done) {
                let d = document.createElement(htmlTAG);

                let div = document.getElementById('test1');
                div.append(d);

                expect(d.shadowRoot.innerHTML).is.eq('<h1>test</h1>');
                done();

            })
        })

        /**
         * @link https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/113
         */
        describe('Assign CSSStyle as Array and CSS as string', function () {

            const htmlTAG = 'monster-testclass-x113-21';

            let mocks, TestComponentX113X22;
            beforeEach(() => {

                mocks = document.getElementById('mocks');
                mocks.innerHTML = html1;

                TestComponentX113X22 = class extends CustomElement {
                    static getTag() {
                        return htmlTAG
                    }

                    /**
                     * @return {Object}
                     */
                    get defaults() {
                        return Object.assign({}, super.defaults, {
                            templates: {main: '<h1>test</h1>'},
                        })
                    }


                    static getCSSStyleSheet() {
                        return 'a { color:red }';
                    }
                }

                registerCustomElement(TestComponentX113X22)


            })

            it(htmlTAG + " should eq <style>a { color:red }</style><h1>test</h1>", function (done) {
                let d = document.createElement(htmlTAG);

                let div = document.getElementById('test1');
                div.append(d);


                expect(d.shadowRoot.innerHTML).is.eq('<style>a { color:red }</style><h1>test</h1>');
                done();

            })
        })
        /**
         * @link https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/113
         */
        describe('Assign CSSStyle as Array and CSS as string', function () {

            const htmlTAG = 'monster-testclass-x113-22';

            let mocks, TestComponentX113X223;
            beforeEach(() => {

                mocks = document.getElementById('mocks');
                mocks.innerHTML = html1;

                TestComponentX113X223 = class extends CustomElement {
                    static getTag() {
                        return htmlTAG
                    }

                    /**
                     * @return {Object}
                     */
                    get defaults() {
                        return Object.assign({}, super.defaults, {
                            templates: {main: '<h1>test</h1>'},
                        })
                    }


                    static getCSSStyleSheet() {
                        return ['a { color:red }'];
                    }
                }

                registerCustomElement(TestComponentX113X223)


            })

            it(htmlTAG + " should eq <style>a { color:red }</style><h1>test</h1>", function (done) {
                let d = document.createElement(htmlTAG);

                let div = document.getElementById('test1');
                div.append(d);


                expect(d.shadowRoot.innerHTML).is.eq('<style>a { color:red }</style><h1>test</h1>');
                done();

            })
        })

	        describe('hasNode()', function () {

            let mocks;
            beforeEach(() => {

                mocks = document.getElementById('mocks');
                mocks.innerHTML = html1;

            })

            it("hasNode monster-testclass should return ...", function () {
                let d = document.createElement('monster-testclass');

                let p1 = document.createElement('p');
                let t1 = document.createTextNode('test1');
                p1.appendChild(t1);

                let p = document.createElement('div');
                let t = document.createTextNode('test');
                p.appendChild(p1);
                p.appendChild(t);
                d.appendChild(p);

                let div = document.getElementById('test1');
                div.append(d);


                let n1 = document.createElement('p');

                expect(d.hasNode(n1)).to.be.false;
                expect(d.hasNode(t)).to.be.true;
                expect(d.hasNode(p)).to.be.true;
                expect(d.hasNode(p1)).to.be.true;
                expect(d.hasNode(t1)).to.be.true;

            })

            it("hasNode monster-testclass2 should return ...", function () {
                let d = document.createElement('monster-testclass2');

                let p1 = document.createElement('p');
                let t1 = document.createTextNode('test1');
                p1.appendChild(t1);

                let p = document.createElement('div');
                let t = document.createTextNode('test');
                p.appendChild(p1);
                p.appendChild(t);


                let div = document.getElementById('test1');
                div.append(d);

                let a = d.shadowRoot.getElementById('container');

                d.shadowRoot.getElementById('container').appendChild(p);

                let n1 = document.createElement('p');

                expect(d.hasNode(n1)).to.be.false;
                expect(d.hasNode(t)).to.be.true;
                expect(d.hasNode(p)).to.be.true;
                expect(d.hasNode(p1)).to.be.true;
                expect(d.hasNode(t1)).to.be.true;

            })


	        })

	        describe('mutation observer lifecycle', function () {
	            it('should not rerun linked updaters after disconnect', function (done) {
	                let mocks = document.getElementById('mocks');
	                mocks.innerHTML = `<monster-testclass-mutation id="mutation-case"></monster-testclass-mutation>`;

	                const element = document.getElementById('mutation-case');

	                setTimeout(() => {
	                    try {
	                        const updaterGroups = element[customElementUpdaterLinkSymbol];
	                        expect(updaterGroups).to.be.instanceof(Set);

	                        let linkedUpdater = null;
	                        for (const group of updaterGroups) {
	                            for (const updater of group) {
	                                linkedUpdater = updater;
	                                break;
	                            }
	                            if (linkedUpdater) {
	                                break;
	                            }
	                        }

	                        expect(linkedUpdater).to.exist;

	                        let runCount = 0;
	                        const originalRun = linkedUpdater.run.bind(linkedUpdater);
	                        linkedUpdater.run = function () {
	                            runCount++;
	                            return originalRun();
	                        };

	                        mocks.removeChild(element);
	                        element.appendChild(document.createElement('div'));

	                        setTimeout(() => {
	                            try {
	                                expect(runCount).to.equal(0);
	                                done();
	                            } catch (e) {
	                                done(e);
	                            }
	                        }, 90);
	                    } catch (e) {
	                        done(e);
	                    }
	                }, 20);
	            })
	        })

	    });
})
