
import {IObjectType, IStringType, Model, Types} from "../../lib/index";
import assert from "assert";
import {eol} from "../../lib/utils";

describe("ObjectType", () => {
    
    it("object", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    map: Types.Object
                };
            }
        }
        const AnyModel = SomeModel as any;

        assert.throws(
            () => {
                const someModel = new AnyModel({
                    map: false
                });
            }, 
            (err: Error) => 
                err.message ===  "invalid object {*: any} for map: false"
        );
        

        assert.throws(
            () => {
                const someModel = new AnyModel({
                    map: true
                });
            }, 
            (err: Error) => 
                err.message ===  "invalid object {*: any} for map: true"
        );
        

        assert.throws(
            () => {
                const someModel = new AnyModel({
                    map: "1,2"
                });
            }, 
            (err: Error) => 
                err.message ===  "invalid object {*: any} for map: \"1,2\""
        );
        

        assert.throws(
            () => {
                const someModel = new AnyModel({
                    map: NaN
                });
            }, 
            (err: Error) => 
                err.message ===  "invalid object {*: any} for map: NaN"
        );
        

        assert.throws(
            () => {
                const someModel = new AnyModel({
                    map: /x/
                });
            }, 
            (err: Error) => 
                err.message ===  "invalid object {*: any} for map: /x/"
        );
        
        assert.throws(
            () => {
                const someModel = new AnyModel({
                    map: []
                });
            }, 
            (err: Error) => 
                err.message ===  "invalid object {*: any} for map: []"
        );
        

        assert.throws(
            () => {
                const someModel = new AnyModel({
                    map: Infinity
                });
            }, 
            (err: Error) => 
                err.message ===  "invalid object {*: any} for map: Infinity"
        );
        

        assert.throws(
            () => {
                const someModel = new AnyModel({
                    map: -Infinity
                });
            }, 
            (err: Error) =>
                err.message === "invalid object {*: any} for map: -Infinity"
        );
        

        assert.throws(
            () => {
                const someModel = new AnyModel({
                    map: 0
                });
            }, 
            (err: Error) => 
                err.message ===  "invalid object {*: any} for map: 0"
        );
        


        assert.throws(
            () => {
                const someModel = new AnyModel({
                    map: [false]
                });
            }, 
            (err: Error) => 
                err.message === "invalid object {*: any} for map: [false]"
        );
        


        const outsideObj = {
            a: "1",
            b: "2"
        };
        const model = new SomeModel({
            map: outsideObj
        });

        const map = model.get("map")!;

        assert.deepEqual( map, outsideObj );

        assert.ok( outsideObj !== map );


        // object should be frozen
        assert.throws(
            () => {
                map.a = 10;
            },
            (err: Error) =>
                /Cannot assign to read only property/.test(err.message)
        );
        

        assert.deepEqual( map, outsideObj );
    });

    
    
    it("emptyAsNull", () => {
        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    words: Types.Object({
                        element: Types.Any,
                        emptyAsNull: true
                    })
                };
            }
        }

        const model = new SomeModel();
        assert.strictEqual( model.row.words, null );

        model.set({words: {red: true}});
        assert.deepEqual( model.row.words, {red: true} );

        model.set({words: {}});
        assert.strictEqual( model.row.words, null );
    });

    it("nullAsEmpty", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    words: Types.Object({
                        element: Types.Any,
                        nullAsEmpty: true
                    })
                };
            }
        }

        const model = new SomeModel();
        assert.deepEqual( model.row.words, [] );

        model.set({words: {water: true}});
        assert.deepEqual( model.row.words, {water: true} );

        model.set({words: null as any});
        assert.deepEqual( model.row.words, {} );
    });

    it("recursive clone object", () => {
        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    tree: Types.Object
                };
            }
        }

        const model = new SomeModel({
            tree: {
                name: "test",
                tree: {
                    name: "nice",
                    arr: [{ x: 1 }]
                }
            }
        });

        assert.deepEqual( model.row, {
            tree: {
                name: "test",
                tree: {
                    name: "nice",
                    arr: [{ x: 1 }]
                }
            }
        });

        const clone = model.clone();

        assert.deepEqual( clone.row, {
            tree: {
                name: "test",
                tree: {
                    name: "nice",
                    arr: [{ x: 1 }]
                }
            }
        });

        assert.ok(
            clone.row.tree !== model.row.tree
        );

        assert.ok(
            clone.row.tree!.tree !== model.row.tree!.tree
        );

        assert.ok(
            clone.row.tree!.tree.arr !== model.row.tree!.tree.arr
        );

        assert.ok(
            clone.row.tree!.tree.arr[0] !== model.row.tree!.tree.arr[0]
        );
    });
    
    it("model.toJSON with object property", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    names: Types.Object({
                        element: Types.Boolean
                    })
                };
            }
        }

        const model = new SomeModel({
            names: {
                Bob: true,
                James: true
            }
        });

        assert.deepEqual(
            model.toJSON(),
            {
                names: {
                    Bob: true,
                    James: true
                }
            }
        );
    });

    it("recursive convert object to json", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    tree: Types.Object
                };
            }
        }

        const model = new SomeModel({
            tree: {
                name: "test",
                tree: {
                    name: "nice",
                    arr: [{ x: 1 }]
                }
            }
        });

        const json = model.toJSON();

        assert.deepEqual( json, {
            tree: {
                name: "test",
                tree: {
                    name: "nice",
                    arr: [{ x: 1 }]
                }
            }
        });

        assert.ok(
            json.tree !== model.row.tree
        );

        assert.ok(
            json.tree!.tree !== model.row.tree!.tree
        );

        assert.ok(
            json.tree!.tree.arr !== model.row.tree!.tree.arr
        );

        assert.ok(
            json.tree!.tree.arr[0] !== model.row.tree!.tree.arr[0]
        );
    });

    it("prepare object value", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    object: Types.Object({
                        element: Types.Number
                    })
                };
            }
        }

        const model = new SomeModel({
            object: {
                a: "10" as any,
                b: "20" as any
            }
        });
        assert.strictEqual( model.row.object!.a, 10 );
        assert.strictEqual( model.row.object!.b, 20 );
    });

    it("object of any values", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    object: Types.Object
                };
            }
        }

        const model = new SomeModel({
            object: {
                a: 10,
                b: true,
                c: "nice"
            }
        });

        assert.deepStrictEqual( model.row, {
            object: {
                a: 10,
                b: true,
                c: "nice"
            }
        });
    });

    it("validate element", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    object: Types.Object({
                        element: Types.Number
                    })
                };
            }
        }

        assert.throws(
            () => {
                const model = new SomeModel({
                    object: {
                        a: 10,
                        b: "nice" as any
                    }
                });
            }, 
            (err: Error) => 
                err.message === `invalid value for type number in property object[b] for model field object: {"a":10,"b":"nice"},${eol} invalid number for b: "nice"`
        );
    });

    it("equal objects", () => {
        const pairs: any[][] = [
            [null, null, true],
            [null, {}, false],
            [{}, {}, true],
            [{x: 1}, {x: 1}, true],
            [{x: 1}, {x: 1, y: 2}, false]
        ];

        pairs.forEach((pair) => {
            class TestModel extends Model<TestModel> {
                structure() {
                    return {
                        obj: Types.Object({
                            element: Types.Number
                        })
                    };
                }
            }

            const model1 = new TestModel({
                obj: pair[0]
            });

            const model2 = new TestModel({
                obj: pair[1]
            });

            assert.strictEqual(
                model1.equal( model2 ),
                pair[2],
                JSON.stringify(pair)
            );

            assert.strictEqual(
                model2.equal( model1 ),
                pair[2],
                JSON.stringify(pair)
            );
        });
    });


    it("equal circular objects", () => {
        const circular1: any = {};
        circular1.self = circular1;

        const circular2: any = {};
        circular2.self = circular2;

        const circular3: any = {};
        circular3.self = circular3;
        circular3.x = {};

        const pairs = [
            [circular1, circular1, true],
            [circular1, circular2, true],
            [circular2, circular2, true],
            [circular1, circular3, false],
            [circular2, circular3, false]
        ];


        pairs.forEach((pair) => {
            class TestModel extends Model<TestModel> {
                structure() {
                    return {
                        obj: Types.Object({
                            element: Types.Object({
                                element: Types.Object
                            })
                        })
                    };
                }
            }

            const model1 = new TestModel({
                obj: pair[0]
            });

            const model2 = new TestModel({
                obj: pair[1]
            });

            assert.strictEqual(
                model1.equal( model2 ),
                pair[2],
                pair.toString()
            );

            assert.strictEqual(
                model2.equal( model1 ),
                pair[2],
                pair.toString()
            );
        });
    });

    
    // when in type defined BaseModel, and in row we have ChildModel (extends BaseModel), 
    // then clone should be instance of ChildModel
    it("clone object of models, should return object of instance of Child", () => {
        
        class FirstLevel extends Model<FirstLevel> {
            structure() {
                return {
                    
                };
            }
        }

        class SecondLevel extends FirstLevel {
            structure() {
                return {
                    level: Types.Number({
                        default: 2
                    })
                };
            }
        }

        class MainModel extends Model<MainModel> {
            structure() {
                return {
                    obj: Types.Object({
                        element: FirstLevel
                    })
                };
            }
        }

        const second = new SecondLevel();
        const main = new MainModel({
            obj: {prop: second}
        });

        assert.deepEqual(
            main.toJSON(),
            {
                obj: {prop: {
                    level: 2
                }}
            }
        );

        const clone = main.clone();

        assert.ok( clone.get("obj")!.prop instanceof SecondLevel );

    });

    it("conflicting parameters: nullAsEmpty and emptyAsNull", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    obj: Types.Object({
                        element: Types.Any,
                        nullAsEmpty: true,
                        emptyAsNull: true
                    })
                };
            }
        }

        assert.throws(
            () => {
                const model = new SomeModel();
            },
            (err: Error) =>
                err.message === "obj: conflicting parameters: use only nullAsEmpty or only emptyAsNull"
        );
    });

    it("circular structure to json", () => {
        
        class MyModel extends Model<MyModel> {
            structure() {
                return {
                    obj: Types.Object({
                        element: Types.Object({
                            element: Types.Object
                        })
                    })
                };
            }
        }

        const obj: any = {};
        obj.obj = obj;

        const model = new MyModel({
            obj
        });

        assert.throws(
            () => {
                model.toJSON();
            },
            (err: Error) =>
                err.message === "Cannot converting circular structure to JSON"
        );
    });

    it("circular structure to json (object of models)", () => {
        
        class MyModel extends Model<MyModel> {
            structure(): {
                obj: IObjectType<MyModel>;
            } {
                return {
                    obj: Types.Object({
                        element: MyModel
                    })
                };
            }
        }

        const model = new MyModel();
        model.set({
            obj: {
                model
            }
        });

        assert.throws(
            () => {
                model.toJSON();
            },
            (err: Error) =>
                err.message === "Cannot converting circular structure to JSON"
        );
    });

    it("same model as value in two fields", () => {
        
        class MyModel extends Model<MyModel> {
            structure(): {
                obj: IObjectType<MyModel>
            } {
                return {
                    obj: Types.Object({
                        element: MyModel
                    })
                };
            }
        }

        const child = new MyModel();
        const model = new MyModel({
            obj: {
                a: child,
                b: child
            }
        });

        assert.deepStrictEqual(model.toJSON(), {
            obj: {
                a: {obj: null},
                b: {obj: null}
            }
        });
    });

    it("object of models, check parent reference", () => {

        class TestModel extends Model<TestModel> {
            structure(): {
                name: IStringType;
                children: IObjectType<TestModel>;
            } {
                return {
                    name: Types.String,
                    children: Types.Object({
                        element: TestModel
                    })
                };
            }
        }

        const parent = new TestModel({
            name: "parent",
            children: {
                a: new TestModel({name: "a"}),
                b: new TestModel({name: "b"})
            }
        });

        const a = parent.get("children")!.a as TestModel;
        const b = parent.get("children")!.b as TestModel;

        const aParent = a.findParentInstance(TestModel);
        const bParent = b.findParentInstance(TestModel);

        assert.ok(aParent === parent, "a parent is valid model");
        assert.ok(bParent === parent, "b parent is valid model");
    });

    it("clone object of models, check parent reference", () => {

        class TestModel extends Model<TestModel> {
            structure(): {
                name: IStringType;
                children: IObjectType<TestModel>;
            } {
                return {
                    name: Types.String,
                    children: Types.Object({
                        element: TestModel
                    })
                };
            }
        }

        const parent = new TestModel({
            name: "parent",
            children: {
                a: new TestModel({name: "a"}),
                b: new TestModel({name: "b"})
            }
        });
        const parentClone = parent.clone();

        const aClone = parentClone.get("children")!.a as TestModel;
        const bClone = parentClone.get("children")!.b as TestModel;

        const aCloneParent = aClone.findParentInstance(TestModel);
        const bCloneParent = bClone.findParentInstance(TestModel);

        assert.ok(aCloneParent === parentClone, "a clone parent is valid model");
        assert.ok(bCloneParent === parentClone, "b clone parent is valid model");
    });

});
