import {expect} from "chai"
import {extend} from "../../../source/data/extend.mjs";
import {isObject} from "../../../source/types/is.mjs";
import {Observer} from "../../../source/types/observer.mjs";

import {ProxyObserver} from "../../../source/types/proxyobserver.mjs";

describe('ProxyObserver', function () {

    describe('create', function () {
        it('should return instanceof ProxyObserver', function () {
            let o = new ProxyObserver({});
            extend(o.getSubject(), {a: 4});
            expect(o.getRealSubject()).is.eql({a: 4});
        });
    });

    describe('setSubject', function () {
        it('should run observer', function (done) {
            let o = new ProxyObserver({a: 1});
            expect(o.getRealSubject()).is.eql({a: 1});
            let counter = 0;
            o.attachObserver(new Observer(function () {
                if (isObject(this) && this instanceof ProxyObserver) {
                    counter++;
                    // only one change as notify runs later
                    if (counter === 1) {
                        done();
                        return;
                    }
                    done("called for error");
                    return;
                }

                done(new Error("this is not ProxyObserver"))
            }))

            o.getSubject().c = 4;
            expect(o.getRealSubject()).is.eql({a: 1, c: 4});

            o.setSubject({b: 2});
            expect(o.getRealSubject()).is.eql({b: 2});

            o.getSubject().d = 5;
            expect(o.getRealSubject()).is.eql({b: 2, d: 5});


        });
    });

    describe('create', function () {
        it('should return instanceof ProxyObserver', function () {
            let o = new ProxyObserver({});
            expect(o).is.instanceOf(ProxyObserver);
        });
    });

    describe('proxy objects', function () {
        it('Proxy objects should not be recommitted', function () {
            let o = new ProxyObserver({a: {b: true}});
            o.getSubject().a.b = o.getSubject();
            o.getSubject().a.b.a.b = false;
            //expect(o.getSubject()===p.getSubject()).to.be.true;
        });
    });

    describe('notify observer', function () {
        it('should inform observer', function (done) {
            let counter = 0;
            // the method is called twice, once for defineProperty and for set trap
            let o = new Observer(function (a) {
                if (isObject(this) && this instanceof ProxyObserver) {
                    counter++;
                    if (counter === 1) {
                        done();
                    }
                    return;
                }

                done(new Error("this is not ProxyObserver"))
            }, true)

            let realSubject = {
                a: {
                    b: {
                        c: true
                    },
                    d: 5
                }
            }

            let p = new ProxyObserver(realSubject);
            expect(p).is.instanceOf(ProxyObserver);
            expect(p.attachObserver(o)).is.instanceOf(ProxyObserver);

            let s = p.getSubject();
            let r = p.getRealSubject();

            s.a.d = false;
            s.a.b.d = 2;

        });
    });

    /**
     * @see https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/43
     */
    describe('Reflect', function () {

        it('check Reflect.set() with Subject', function () {
            let r;

            const obj = (new ProxyObserver({})).getSubject();

            [
                "yes!",
                undefined,
                "no"
            ].forEach(function (value) {
                expect(() => {
                    obj['data'] = value
                }).to.not.throw();
                //r = Reflect.set(obj, 'data', value)
                //expect(r).to.be.true;
            })


        });


        [
            ['that', {that: 'checked1'}],
            ['that', new Proxy({that: 'checked2'}, {})],
            ['that', (new ProxyObserver({that: 'checked3'})).getRealSubject()],
            ['that', (new ProxyObserver({that: 'checked4'})).getSubject()]
        ].forEach(function (data) {

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

            it('check Reflect.set(' + key + ',' + JSON.stringify(target) + ') should writable', function () {
                let value;

                [
                    "1",
                    undefined,
                    true,
                    undefined,
                    false,
                    undefined,
                    undefined
                ].forEach(function (values) {
                    expect(Reflect.getOwnPropertyDescriptor(
                        target, key)['writable'], 'writable for ' + value + ' should return true').to.be.true;

                    expect(Reflect.set(target, key, value), 'set for ' + value + ' should return true').to.be.true;

                    expect(Reflect.getOwnPropertyDescriptor(
                        target, key)['writable'], 'writable for ' + value + ' should return true').to.be.true;

                });
            })
        })


    });

    describe('notify observer with changes', function () {

        let original, observer, proxy;

        before(function () {
            original = {};
            proxy = new ProxyObserver(original);

        });

        [
            ['a', 4, JSON.stringify({a: 4})],
            ['b', 9, JSON.stringify({a: 4, b: 9})]
        ].forEach(function (data) {

            let a = data.shift()
            let b = data.shift()
            let c = data.shift()

            it('set key ' + a + ' value ' + b + ' expect ' + c, function (done) {

                let counter = 0;
                observer = new Observer(function () {
                    counter++;
                    expect(JSON.stringify(this.getRealSubject())).is.equal(c);
                    if (counter === 1) {
                        this.detachObserver(observer);
                        done();
                    }
                })

                proxy.attachObserver(observer)
                let subject = proxy.getSubject()

                subject[a] = b;

            });
        });
    })

    describe('notify observer ', function () {

        it('should return promise', function () {
            let proxy = new ProxyObserver({});
            let observer = new Observer(function () {
            })
            proxy.attachObserver(observer)
            expect(proxy.notifyObservers()).is.instanceOf(Promise);
        });
    });

    describe('setPrototypeOf', function () {
        it('should allow setPrototypeOf and notify observer', function (done) {
            const proxy = new ProxyObserver({});
            const observer = new Observer(function () {
                done();
            });
            proxy.attachObserver(observer);

            const proto = {flag: true};
            Object.setPrototypeOf(proxy.getSubject(), proto);
            expect(Object.getPrototypeOf(proxy.getSubject())).to.equal(proto);
        });
    });

    describe('deleteProperty', function () {
        it('should notify observers when deleting a key', function (done) {
            const proxy = new ProxyObserver({a: 1});
            const observer = new Observer(function () {
                expect(proxy.getRealSubject()).to.eql({});
                done();
            });

            proxy.attachObserver(observer);
            delete proxy.getSubject().a;
        });
    });

    describe('symbol properties', function () {
        it('should not notify observers for symbol keys', function (done) {
            const proxy = new ProxyObserver({});
            let called = false;
            const observer = new Observer(function () {
                called = true;
            });

            proxy.attachObserver(observer);
            proxy.getSubject()[Symbol("hidden")] = "value";

            setTimeout(() => {
                expect(called).to.be.false;
                done();
            }, 0);
        });
    });

    describe('runtime compatibility', function () {
        it('should allow nested object access without DOM globals', function () {
            const proxy = new ProxyObserver({options: {enabled: true}});
            expect(proxy.getSubject().options.enabled).to.equal(true);
        });

        it('should preserve setter semantics for accessor-backed instances', function () {
            let setterCalls = 0;

            class Example {
                set value(v) {
                    setterCalls++;
                    this._value = v;
                }

                get value() {
                    return this._value;
                }
            }

            const instance = new Example();
            const proxy = new ProxyObserver({instance});

            proxy.getSubject().instance.value = 7;

            expect(setterCalls).to.equal(1);
            expect(instance.value).to.equal(7);
            expect(Object.prototype.hasOwnProperty.call(instance, 'value')).to.be.false;
        });
    });

})
