
import {Model, Collection, Types, IObjectType} from "../../lib/index";
import assert from "assert";
import {eol} from "../../lib/utils";
import { IArrayType } from "../../lib/type/ArrayType";
import { IStringType } from "../../lib/type/StringType";

describe("Model tests", () => {

    it("create model with row", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    prop: Types.String
                };
            }
        }

        const row = {
            prop: "nice"
        };
        const model = new SomeModel(row);

        assert.strictEqual( model.get("prop"), "nice" );
        assert.strictEqual( model.row.prop, "nice" );

        assert.ok( model.row !== row );
    });

    it("create model without row", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    prop: Types.String
                };
            }
        }

        const model = new SomeModel();

        assert.strictEqual( model.get("prop"), null );
        assert.strictEqual( model.row.prop, null );
    });

    it("default value", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    prop: Types.String({
                        default: "default"
                    })
                };
            }
        }

        let model = new SomeModel();
        assert.equal( model.get("prop"), "default" );
        assert.equal( model.row.prop, "default" );

        model = new SomeModel({});
        assert.equal( model.get("prop"), "default" );
        assert.equal( model.row.prop, "default" );

        model = new SomeModel({});
        assert.equal( model.get("prop"), "default" );
        assert.equal( model.row.prop, "default" );

        model = new SomeModel({
            prop: null as any
        });
        assert.strictEqual( model.get("prop"), null );
        assert.strictEqual( model.row.prop, null );


        model = new SomeModel({
            prop: undefined
        });
        assert.strictEqual( model.get("prop"), null );
        assert.strictEqual( model.row.prop, null );
    });

    it("default() value", () => {

        const now = Date.now();

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    now: Types.Number({
                        default: () => Date.now()
                    })
                };
            }
        }

        const model = new SomeModel();

        assert.ok(
            model.row.now! >= now
        );
    });

    it("set value", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    name: Types.String,
                    age: Types.Number
                };
            }
        }

        const model = new SomeModel();
        let row = model.row;

        assert.strictEqual( model.get("name"), null );
        assert.strictEqual( model.row.name, null );
        assert.strictEqual( model.get("age"), null );
        assert.strictEqual( model.row.age, null );
        
        model.set({name: "nice"});
        assert.equal( model.get("name"), "nice" );
        assert.equal( model.row.name, "nice" );
        assert.strictEqual( model.get("age"), null );
        assert.strictEqual( model.row.age, null );
        
        assert.ok( row !== model.row );
        row = model.row;

        model.set({name: null as any});
        assert.strictEqual( model.get("name"), null );
        assert.strictEqual( model.row.name, null );
        assert.strictEqual( model.get("age"), null );
        assert.strictEqual( model.row.age, null );

        assert.ok( row !== model.row );
        row = model.row;

        model.set({name: "test"});
        assert.equal( model.get("name"), "test" );
        assert.equal( model.row.name, "test" );
        assert.strictEqual( model.get("age"), null );
        assert.strictEqual( model.row.age, null );

        assert.ok( row !== model.row );
        row = model.row;

        model.set({age: 101});
        assert.equal( model.get("name"), "test" );
        assert.equal( model.row.name, "test" );
        assert.strictEqual( model.get("age"), 101 );
        assert.strictEqual( model.row.age, 101 );


        model.set({
            name: "Good",
            age: 99
        });
        assert.equal( model.get("name"), "Good" );
        assert.equal( model.row.name, "Good" );
        assert.strictEqual( model.get("age"), 99 );
        assert.strictEqual( model.row.age, 99 );
    });

    it("error on set unknown property", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    prop: Types.String
                };
            }
        }

        const model = new SomeModel();

        model.set({prop: "x"});
        assert.equal( model.get("prop"), "x" );
        assert.equal( model.row.prop, "x" );
        
        const row = model.row;

        assert.throws(
            () => {
                const anyModel = model as any;
                anyModel.set({some: "1"});
            }, (err: Error) =>
                err.message === "unknown property: some"
        );

        // invalid action cannot change object
        assert.equal( model.get("prop"), "x" );
        assert.equal( model.row.prop, "x" );

        assert.ok( model.row === row );


        assert.throws(
            () => {
                const AnyModel = SomeModel as any;
                const someModel = new AnyModel({
                    some: "x"
                });
            },
            (err: Error) =>
                err.message === "unknown property: some"
        );
    });


    it("model.row is freeze object", () => {
        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    prop: Types.String
                };
            }
        }

        const model = new SomeModel();

        assert.throws(
            () => {
                const anyModel = model as any;
                anyModel.row.prop = "a";
            }, 
            (err: Error) =>
                /Cannot assign to read only property/.test(err.message)
        );

        model.set({prop: "x"});
        assert.equal( model.get("prop"), "x" );
        assert.equal( model.row.prop, "x" );

        assert.throws(
            () => {
                const anyModel = model as any;
                anyModel.row.prop = "y";
            }, (err: Error) =>
                /Cannot assign to read only property/.test(err.message)
        );

        assert.equal( model.get("prop"), "x" );
        assert.equal( model.row.prop, "x" );
    });

    it("keep row if hasn't changes", () => {
        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    prop: Types.String
                };
            }
        }

        const model = new SomeModel({
            prop: "value"
        });

        const row = model.row;

        model.set({prop: "value"});
        assert.ok( model.row === row );

        model.set({
            prop: "value"
        });
        assert.ok( model.row === row );
    });

    it("model.hasProperty", () => {
        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    prop: Types.String
                };
            }
        }

        const model = new SomeModel();

        assert.strictEqual(
            model.hasProperty("prop"),
            true
        );

        const anyModel = model as any;
        assert.strictEqual(
            anyModel.hasProperty("unknown"),
            false
        );

        assert.strictEqual(
            anyModel.hasProperty("hasOwnProperty"),
            false
        );
    });

    it("model.hasValue", () => {
        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    name: Types.String,
                    age: Types.Number
                };
            }
        }

        const model = new SomeModel({
            name: "Bob"
        });

        assert.strictEqual(
            model.hasValue("name"),
            true
        );

        assert.strictEqual(
            model.hasValue("age"),
            false
        );

        model.set({
            name: null as any,
            age: 100
        });

        assert.strictEqual(
            model.hasValue("name"),
            false
        );

        assert.strictEqual(
            model.hasValue("age"),
            true
        );


        // unknown prop
        const anyModel = model as any;
        assert.strictEqual(
            anyModel.hasValue("prop"),
            false
        );
    });

    it("model.toJSON", () => {
        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    name: Types.String,
                    age: Types.Number
                };
            }
        }

        const model = new SomeModel();

        assert.deepEqual(
            model.toJSON(),
            {
                name: null,
                age: null
            }
        );

        model.set({
            name: "",
            age: 0
        });

        assert.deepEqual(
            model.toJSON(),
            {
                name: "",
                age: 0
            }
        );
    });

    it("model.prepareJSON", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    name: Types.String,
                    age: Types.Number
                };
            }

            prepareJSON(json: any) {
                delete json.age;
            }
        }

        const model = new SomeModel();

        assert.deepEqual(
            model.toJSON(),
            {
                name: null
            }
        );

        model.set({
            name: "",
            age: 0
        });

        assert.deepEqual(
            model.toJSON(),
            {
                name: ""
            }
        );
    });

    it("model.clone() with required field", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    name: Types.String({
                        required: true
                    })
                };
            }
        }

        const model = new SomeModel({
            name: "test"
        });

        assert.deepEqual(model.row, {
            name: "test"
        });

        const clone = model.clone();

        assert.deepEqual(clone.row, {
            name: "test"
        });
    });

    
    it("extends from another Model with row", () => {

        class FirstLevel extends Model<FirstLevel> {
            structure() {
                return {
                    lvl1: Types.String
                };
            }
        }

        class SecondLevel extends Model<SecondLevel> {
            structure() {
                return {
                    ...(new FirstLevel().structure()),
                    lvl2: Types.String
                };
            }
        }

        // create models without errors
        const first = new FirstLevel({ lvl1: "1"});
        const second = new SecondLevel({ lvl2: "2" });
    });

    it("custom toJSON, for field", () => {

        class SomeModel extends Model<SomeModel> {
            structure(): {
                name: IStringType;
                self: typeof SomeModel;
            } {
                return {
                    name: Types.String,
                    self: Types.Model({
                        Model: SomeModel,
                        toJSON: (selfModel) =>
                            selfModel.get("name")
                    })
                };
            }
        }

        const model = new SomeModel({
            name: "circular"
        });

        model.set({self: model});

        assert.deepEqual(
            model.toJSON(),
            {
                name: "circular",
                self: "circular"
            }
        );
    });

    it("custom toJSON, for any field", () => {

        class SomeModel extends Model<SomeModel> {
            structure(): {
                [key: string]: typeof SomeModel;
            } {
                return {
                    "*": Types.Model({
                        Model: SomeModel,
                        toJSON: () =>
                            "nice"
                    })
                };
            }
        }

        const model = new SomeModel();
        model.set({self: model});
        model.set({empty: null  as any});

        assert.deepEqual(
            model.toJSON(),
            {
                self: "nice",
                empty: null
            }
        );
    });

    it("clone, for any field", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    "*": Types.Any
                };
            }
        }

        const model = new SomeModel();
        model.set({some: 12});
        model.set({empty: null});

        assert.deepEqual(
            model.clone().row,
            {
                some: 12,
                empty: null
            }
        );
    });

    
    it("custom prepare field", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    money: Types.Number({
                        default: 1,
                        prepare: (value) =>
                            value * 2
                    })
                };
            }
        }

        const model = new SomeModel();
        assert.strictEqual( model.row.money, 2 );

        model.set({money: 12});
        assert.strictEqual( model.row.money, 24 );

        model.set({money: null as any});
        assert.strictEqual( model.row.money, null );
    });

    it("check value type after custom prepare", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    name: Types.String
                };
            }

            prepare(row: any) {
                row.name = 10;
            }
        }

        const model = new SomeModel();
        assert.strictEqual( model.row.name, "10" );

        model.set({name: (12 as any)});
        assert.strictEqual( model.row.name, "10" );
    });

    it("check value type after custom prepare (any key)", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    "*": Types.String
                };
            }

            prepare(row: any) {
                row.name = 10;
            }
        }

        const model = new SomeModel();
        assert.strictEqual( model.row.name, "10" );

        model.set({name: (12 as any)});
        assert.strictEqual( model.row.name, "10" );
    });

    it("custom prepare field and standard prepares (round, trim, emptyAsNull)", () => {
        interface ISomeData {
            name: string;
            age: number;
        }

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    name: Types.String({
                        default: "  ",
                        trim: true,
                        emptyAsNull: true,
                        prepare: (value) =>
                            value[0].toUpperCase() + 
                                value.slice(1).toLowerCase()
                    }),
                    age: Types.Number({
                        default: 0,
                        zeroAsNull: true,
                        prepare: (value) =>
                            +(value).toFixed(0)
                    })
                };
            }
        }

        let model = new SomeModel();
        assert.strictEqual( model.row.name, null );
        assert.strictEqual( model.row.age, null );

        model = new SomeModel({
            name: " wOrd ",
            age: 1.1111
        });
        assert.strictEqual( model.row.name, "Word" );
        assert.strictEqual( model.row.age, 1 );
    });

    it("custom prepare row", () => {
        interface ISomeData {
            firstName: string;
            lastName: string;
            fullName: string;
        }

        function upFirstLetter(name: string) {
            return (
                name[0].toUpperCase() + 
                    name.slice(1).toLowerCase()
            );
        }

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    firstName: Types.String({
                        trim: true,
                        emptyAsNull: true,
                        prepare: upFirstLetter
                    }),
                    lastName: Types.String({
                        trim: true,
                        emptyAsNull: true,
                        prepare: upFirstLetter
                    }),
                    fullName: Types.String({
                        trim: true,
                        emptyAsNull: true
                    })
                };
            }

            prepare(row: any) {
                // row.firstName can be null
                const firstName = row.firstName || "";
                // row.lastName can be null
                const lastName = row.lastName || "";

                row.fullName = `${ firstName } ${lastName}`;
            }
        }

        let model = new SomeModel();
        assert.strictEqual( model.row.firstName, null );
        assert.strictEqual( model.row.lastName, null );
        assert.strictEqual( model.row.fullName, null );

        model = new SomeModel({
            firstName: "bob"
        });
        assert.strictEqual( model.row.firstName, "Bob" );
        assert.strictEqual( model.row.lastName, null );
        assert.strictEqual( model.row.fullName, "Bob" );
        
        model.set({
            lastName: "  taylor"
        });
        assert.strictEqual( model.row.firstName, "Bob" );
        assert.strictEqual( model.row.lastName, "Taylor" );
        assert.strictEqual( model.row.fullName, "Bob Taylor" );
    });

    
    it("custom required field", () => {

        class FileModel extends Model<FileModel> {
            structure() {
                return {
                    name: Types.String({
                        required: true
                    }),
                    path: Types.String
                };
            }

            prepare(row: any) {
                row.name = row.path.split("/").pop();
            }
        }

        const file = new FileModel({
            path: "test/name.txt"
        });
        assert.strictEqual( file.row.name, "name.txt" );
    });

    it("required field with default value", () => {

        class FileModel extends Model<FileModel> {
            structure() {
                return {
                    name: Types.String({
                        required: true,
                        default: "nice"
                    }),
                    path: Types.String
                };
            }
        }

        const file = new FileModel({
            path: "test/name.txt"
        });
        assert.strictEqual( file.row.name, "nice" );
    });

    it("equal with same class model", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    prop: Types.String
                };
            }
        }

        const firstModel = new SomeModel({
            prop: "first"
        });
        const secondModel = new SomeModel({
            prop: "second"
        });

        assert.ok( !firstModel.equal( secondModel ) );
        assert.ok( !secondModel.equal( firstModel ) );

        secondModel.set({prop: "first"});

        assert.ok( firstModel.equal( secondModel ) );
        assert.ok( secondModel.equal( firstModel ) );
    });

    it("equal model with another row", () => {

        class Model1 extends Model<Model1> {
            structure() {
                return {
                    some: Types.Number
                };
            }
        }

        class Model2 extends Model<Model2> {
            structure() {
                return {
                    "*": Types.Any
                };
            }
        }


        const firstModel = new Model1({
            some: 1
        });
        const secondModel = new Model2({
            some: 1
        });

        assert.ok( firstModel.equal( secondModel as any ) );
        assert.ok( secondModel.equal( firstModel ) );

        secondModel.set({another: "value"});

        assert.ok( !firstModel.equal( secondModel as any ) );
        assert.ok( !secondModel.equal( firstModel ) );
    });


    it("equal models with same models in two fields", () => {

        class User extends Model<User> {
            structure() {
                return {
                    name: Types.String
                };
            }
        }
        class TestModel extends Model<TestModel> {
            structure() {
                return {
                    a: User,
                    b: User
                };
            }
        }

        const user = new User({
            name: "Jack"
        });
        const firstModel = new TestModel({
            a: user,
            b: user
        });
        const secondModel = new TestModel({
            a: user,
            b: user
        });

        assert.ok( firstModel.equal( secondModel ) );
        assert.ok( secondModel.equal( firstModel ) );
    });

    it("max error length on prepare", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    prop: Types.Number
                };
            }
        }

        assert.throws(
            () => {
                const model = new SomeModel({
                    prop: ("X1234568790123456879012345687901234568790123456879" as any)
                });
            }, (err: Error) =>
                err.message === "invalid number for prop: \"X1234568790123456879012345687901234568790123456879\""
        );

        assert.throws(
            () => {
                const model = new SomeModel({
                    prop: ("X1234568790123456879012345687901234568790123456879y" as any)
                });
            }, (err: Error) =>
                err.message === "invalid number for prop: \"X1234568790123456879012345687901234568790123456879...\""
        );

        assert.throws(
            () => {
                const AnyModel = SomeModel as any;
                const model = new AnyModel({
                    prop: {some: 1}
                });
            }, (err: Error) =>
            err.message === "invalid number for prop: {\"some\":1}"
            );
            
        assert.throws(
            () => {
                const AnyModel = SomeModel as any;
                const model = new AnyModel({
                    prop: {some: 1, sub: {bigString: "X1234568790123456879012345687901234568790123456879"}}
                });
            }, (err: Error) =>
                err.message === "invalid number for prop: {\"some\":1,\"sub\":{\"bigString\":\"X1234568790123456879..."
        );
    });

    it("test eol on linux", () => {

        eol.define( "linux" );

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    arr: Types.Array({
                        element: Types.Number
                    }),
                    obj: Types.Object({
                        element: Types.Number
                    })
                };
            }
        }

        assert.throws(
            () => {
                const AnyModel = SomeModel as any;
                const model = new AnyModel({
                    arr: ["wrong"]
                });
            }, (err: Error) =>
                err.message === "invalid element for array number[] at 0 for model field arr: [\"wrong\"],\n invalid number for 0: \"wrong\""
        );

        assert.throws(
            () => {
                const AnyModel = SomeModel as any;
                const model = new AnyModel({
                    obj: {prop: "wrong"}
                });
            }, (err: Error) =>
                err.message === "invalid value for type number in property object[prop] for model field obj: {\"prop\":\"wrong\"},\n invalid number for prop: \"wrong\""
        );
    });

    it("test eol on windows", () => {

        eol.define( "windows" );

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    arr: Types.Array({
                        element: Types.Number
                    }),
                    obj: Types.Object({
                        element: Types.Number
                    })
                };
            }
        }

        assert.throws(
            () => {
                const AnyModel = SomeModel as any;
                const model = new AnyModel({
                    arr: ["wrong"]
                });
            }, (err: Error) =>
                err.message === "invalid element for array number[] at 0 for model field arr: [\"wrong\"],\r\n invalid number for 0: \"wrong\""
        );

        assert.throws(
            () => {
                const AnyModel = SomeModel as any;
                const model = new AnyModel({
                    obj: {prop: "wrong"}
                });
            }, (err: Error) =>
                err.message === "invalid value for type number in property object[prop] for model field obj: {\"prop\":\"wrong\"},\r\n invalid number for prop: \"wrong\""
        );
    });

    it("unknown type", () => {
        class AnyModel extends Model<AnyModel> {
            structure() {
                return {
                    x: {
                        type: "test"
                    } as any
                };
            }
        }

        assert.throws(
            () => {
                const model = new AnyModel();
            }, (err: Error) =>
                err.message === "field x, unknown type: test"
        );
    });

    it("wrong validate", () => {
        class AnyModel extends Model<AnyModel> {
            structure() {
                return {
                    x: {
                        type: "string",
                        validate: NaN
                    } as any
                };
            }
        }

        assert.throws(
            () => {
                const model = new AnyModel();
            }, (err: Error) =>
                err.message === "x: validate should be function or RegExp: NaN"
        );
    });

    it("wrong validate key", () => {
        class AnyModel extends Model<AnyModel> {
            structure() {
                return {
                    "*": {
                        type: "string",
                        key: NaN
                    } as any
                };
            }
        }

        assert.throws(
            () => {
                const model = new AnyModel();
            }, (err: Error) =>
                err.message === "*: key validation should be function or RegExp: NaN"
        );
    });

    it("Model without structure method", () => {
        const AnyModel = Model as any;
        class Company extends AnyModel {
        }

        assert.throws(
            () => {
                const model = new Company();
            }, (err: Error) =>
                err.message === "Company.structure() is not declared"
        );
    });

    it("model.clone() with Or type, model value should be same instance like are original", () => {

        class Manager extends Model<Manager> {
            structure() {
                return {
                    name: Types.String,
                    phone: Types.String
                };
            }
        }

        class Client extends Model<Client> {
            structure() {
                return {
                    name: Types.String,
                    phone: Types.String
                };
            }
        }

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    user: Types.Or({
                        or: [Manager, Client]
                    })
                };
            }
        }

        const client = new Client({
            name: "Client",
            phone: "1"
        });
        const manager = new Manager({
            name: "Manager",
            phone: "1"
        });

        const model = new SomeModel({
            user: client
        });

        const clone1 = model.clone();
        assert.ok( clone1.get("user") instanceof Client );
        assert.ok( clone1.get("user") !== model.get("user") );
        assert.deepStrictEqual(model.toJSON(), clone1.toJSON());


        model.set({
            user: manager
        });

        const clone2 = model.clone();
        assert.ok( clone2.get("user") instanceof Manager );
        assert.ok( clone2.get("user") !== model.get("user") );
        assert.deepStrictEqual(model.toJSON(), clone2.toJSON());
    });

    it("model.clone() with Any type, model value should be same instance like are original", () => {

        class Manager extends Model<Manager> {
            structure() {
                return {
                    name: Types.String,
                    phone: Types.String
                };
            }
        }

        class Client extends Model<Client> {
            structure() {
                return {
                    name: Types.String,
                    phone: Types.String
                };
            }
        }

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    user: Types.Any
                };
            }
        }

        const client = new Client({
            name: "Client",
            phone: "1"
        });
        const manager = new Manager({
            name: "Manager",
            phone: "1"
        });

        const model = new SomeModel({
            user: client
        });

        const clone1 = model.clone();
        assert.ok( clone1.get("user") instanceof Client );
        assert.ok( clone1.get("user") !== model.get("user") );
        assert.deepStrictEqual(model.toJSON(), clone1.toJSON());


        model.set({
            user: manager
        });

        const clone2 = model.clone();
        assert.ok( clone2.get("user") instanceof Manager );
        assert.ok( clone2.get("user") !== model.get("user") );
        assert.deepStrictEqual(model.toJSON(), clone2.toJSON());
    });

    it("model.clone() with Any type, model value inside array should be same instance like are original", () => {

        class Manager extends Model<Manager> {
            structure() {
                return {
                    name: Types.String,
                    phone: Types.String
                };
            }
        }

        class Client extends Model<Client> {
            structure() {
                return {
                    name: Types.String,
                    phone: Types.String
                };
            }
        }

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    users: Types.Any
                };
            }
        }

        const client = new Client({
            name: "Client",
            phone: "1"
        });
        const manager = new Manager({
            name: "Manager",
            phone: "1"
        });

        const model = new SomeModel({
            users: [[client]]
        });

        const clone1 = model.clone();
        assert.ok( clone1.get("users")[0][0] instanceof Client );
        assert.ok( clone1.get("users")[0][0] !== model.get("users")[0][0] );
        assert.deepStrictEqual(model.toJSON(), clone1.toJSON());


        model.set({
            users: [[manager]]
        });

        const clone2 = model.clone();
        assert.ok( clone2.get("users")[0][0] instanceof Manager );
        assert.ok( clone2.get("users")[0][0] !== model.get("users")[0][0] );
        assert.deepStrictEqual(model.toJSON(), clone2.toJSON());
    });

    it("model.clone() with Any type, model value inside object should be same instance like are original", () => {

        class Manager extends Model<Manager> {
            structure() {
                return {
                    name: Types.String,
                    phone: Types.String
                };
            }
        }

        class Client extends Model<Client> {
            structure() {
                return {
                    name: Types.String,
                    phone: Types.String
                };
            }
        }

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    users: Types.Any
                };
            }
        }

        const client = new Client({
            name: "Client",
            phone: "1"
        });
        const manager = new Manager({
            name: "Manager",
            phone: "1"
        });

        const model = new SomeModel({
            users: {user: client}
        });

        const clone1 = model.clone();
        assert.ok( clone1.get("users").user instanceof Client );
        assert.ok( clone1.get("users").user !== model.get("users").user );
        assert.deepStrictEqual(model.toJSON(), clone1.toJSON());


        model.set({
            users: {user: manager}
        });

        const clone2 = model.clone();
        assert.ok( clone2.get("users").user instanceof Manager );
        assert.ok( clone2.get("users").user !== model.get("users").user );
        assert.deepStrictEqual(model.toJSON(), clone2.toJSON());
    });

    it("model.clone() with circular reference (array type)", () => {
        class Human extends Model<Human> {
            structure(): {
                name: IStringType;
                dad: typeof Human;
                mom: typeof Human;
                children: IArrayType<Human>;
            } {
                return {
                    name: Types.String,
                    dad: Human,
                    mom: Human,
                    children: Types.Array({
                        element: Human
                    })
                };
            }
        }

        const dad = new Human({
            name: "dad"
        });
        const mom = new Human({
            name: "mom"
        });
        const child = new Human({
            name: "bob",
            mom,
            dad
        });

        mom.set({
            children: [child]
        });
        dad.set({
            children: [child]
        });

        const clone = mom.clone();
        assert.ok( clone instanceof Human );
        assert.strictEqual( clone.get("name"), "mom" );
        
        const cloneChild = clone.get("children")![0];
        assert.ok( cloneChild instanceof Human );
        assert.ok( cloneChild !== child );
        assert.strictEqual( cloneChild.get("name"), "bob" );

    });

    
    it("model.clone() with circular reference (object type)", () => {
        class Human extends Model<Human> {
            structure(): {
                name: IStringType;
                friend: IObjectType<Human>;
            } {
                return {
                    name: Types.String,
                    friend: Types.Object({
                        element: Human
                    })
                };
            }
        }

        const bob = new Human({
            name: "bob"
        });
        const jack = new Human({
            name: "jack",
            friend: {first: bob}
        });

        bob.set({
            friend: {first: jack}
        });

        const clone = bob.clone();
        assert.ok( clone instanceof Human );
        assert.strictEqual( clone.get("name"), "bob" );
        
        const cloneFriend = clone.get("friend")!;
        assert.ok( cloneFriend.first instanceof Human, "instance Human" );
        assert.ok( cloneFriend.first !== bob.get("friend")!.first, "is another Model" );
        assert.strictEqual( cloneFriend.first.get("name"), "jack" );

    });

    
    it("model.clone() with circular reference (model type)", () => {
        class Human extends Model<Human> {
            structure() {
                return {
                    name: Types.String,
                    friend: Human
                };
            }
        }

        const bob = new Human({
            name: "bob"
        });
        const jack = new Human({
            name: "jack",
            friend: bob
        });

        bob.set({
            friend: jack
        });

        const clone = bob.clone();
        assert.ok( clone instanceof Human );
        assert.strictEqual( clone.get("name"), "bob" );
        
        const cloneFriend = clone.get("friend")!;
        assert.ok( cloneFriend instanceof Human );
        assert.ok( cloneFriend !== bob.get("friend") );
        assert.strictEqual( cloneFriend.get("name"), "jack" );

    });

    it("model.clone() with circular reference (collection type)", () => {
        class File extends Model<File> {
            structure() {
                return {
                    name: Types.String,
                    files: Files
                };
            }
        }

        class Files extends Collection<File> {
            Model() {
                return File;
            }
        }

        const mainFile = new File({
            name: "main"
        });
        const file1 = new File({
            name: "file 1"
        });
        const file2 = new File({
            name: "file 2"
        });

        const files = new Files([
            file1,
            file2,
            // circular reference
            mainFile
        ]);

        mainFile.set({files});

        const clone = mainFile.clone();
        assert.ok( clone instanceof File );
        assert.ok( clone !== mainFile );
        assert.strictEqual( mainFile.get("name"), clone.get("name") );

        assert.ok( clone.get("files") instanceof Files );
        assert.ok( clone.get("files") !== mainFile.get("files") );

        assert.strictEqual( mainFile.get("files")!.at(1)!.get("name"), "file 2" );


        const filesClone = files.clone();
        assert.ok( filesClone instanceof Files );
        assert.ok( filesClone !== files );

        assert.strictEqual( filesClone.at(1)!.get("name"), "file 2" );
    });

    
    it("model.clone() with circular reference (any type)", () => {
        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    any: Types.Any
                };
            }
        }

        const model1 = new SomeModel();
        const model2 = new SomeModel({
            any: model1
        });
        model1.set({any: model2}); 
        
        const model3 = new SomeModel({
            any: [{any: model1}]
        });

        const clone = model3.clone();
        const cloneModel1 = clone.get("any")[0].any;

        assert.ok( clone instanceof SomeModel );
        assert.ok( cloneModel1 instanceof SomeModel );
        assert.ok( cloneModel1 !== model1 );

        assert.ok( 
            cloneModel1 === cloneModel1.get("any").get("any")
        );
    });
});
