
import {Model, Types, Collection, IStringType, IArrayType} from "../../lib/index";
import assert from "assert";

describe("TreeMethods, walk by children or parents", () => {
    
    it("BinaryTree", () => {
        class BinaryTreeModel extends Model<BinaryTreeModel> {
            structure() {
                return {
                    left: BinaryTreeModel,
                    right: BinaryTreeModel,
                    id: Types.Number,
                    name: Types.String
                };
            }

            findName(id: number): string | null {
                if ( id === this.get("id") ) {
                    return this.get("name")!;
                }

                if ( id > this.get("id") ) {
                    const right = this.get("right") as BinaryTreeModel;
                    if ( right ) {
                        return right.findName(id);
                    }
                }
                else {
                    const left = this.get("left") as BinaryTreeModel;
                    if ( left ) {
                        return left.findName(id);
                    }
                }

                return null;
            }
        }
        
        const binaryTreeModel = new BinaryTreeModel({
            id: 4,
            name: "Bob",
            left: {
                id: 2,
                name: "Jack",
                left: {
                    name: "Oscar",
                    id: 1
                },
                right: {
                    name: "Leo",
                    id: 3
                }
            },
            right: {
                id: 5,
                name: "Harry",
                right: {
                    name: "Oliver",
                    id: 7
                }
            }
        });

        assert.strictEqual( binaryTreeModel.findName(1), "Oscar" );
        assert.strictEqual( binaryTreeModel.findName(2), "Jack" );
        assert.strictEqual( binaryTreeModel.findName(3), "Leo" );
        assert.strictEqual( binaryTreeModel.findName(4), "Bob" );
        assert.strictEqual( binaryTreeModel.findName(5), "Harry" );
        assert.strictEqual( binaryTreeModel.findName(6), null );
        assert.strictEqual( binaryTreeModel.findName(7), "Oliver" );
        assert.strictEqual( binaryTreeModel.findName(8), null );

        // test native method walk
        const nameById: {
            [id: number]: string;
        } = {};
        nameById[ binaryTreeModel.get("id")! ] = binaryTreeModel.get("name")!;

        binaryTreeModel.walk((model: any) => {
            const id = model.get("id");
            const name = model.get("name");

            nameById[ id ] = name;
        });

        assert.deepEqual({
            1: "Oscar",
            2: "Jack",
            3: "Leo",
            4: "Bob",
            5: "Harry",
            7: "Oliver"
        }, nameById);



        let counter = 0;
        binaryTreeModel.walk((model, walker) => {
            counter++;
            walker.exit();
        });

        assert.equal(counter, 1);

        const ids: number[] = [];
        binaryTreeModel.walk((model: any, walker) => {
            ids.push(
                model.get("id")
            );

            if ( model.get("id") === 5 ) {
                walker.continue();
            }
        });
        ids.sort();

        assert.deepEqual(ids, [1, 2, 3, 5]);
    });

    it("walker.skip(), skip one element", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    a: SomeModel,
                    b: SomeModel,
                    c: SomeModel,

                    value: Types.String
                };
            }
        }

        const model = new SomeModel({
            a: {
                a: {
                    value: "a.a"
                },
                value: "a"
            },
            b: {
                b: {
                    value: "b.b"
                },
                value: "b"
            },
            c: {
                c: {
                    value: "c.c"
                },
                value: "c"
            }
        });

        const values: string[] = [];
        model.walk((childModel: any, walker) => {
            const value = childModel.get("value");

            values.push( value );

            if ( value === "b" ) {
                walker.continue();
            }
        });

        values.sort();
        assert.deepEqual(values, [
            "a",
            "a.a",
            "b",
            "c",
            "c.c"
        ]);
    });

    it("findChild", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    child: SomeModel,
                    level: Types.Number
                };
            }
        }

        const model = new SomeModel({
            level: 0,
            child: {
                level: 1,
                child: {
                    level: 2,
                    child: {
                        level: 3
                    }
                }
            }
        });

        let counter = 0;
        const lvl2model = model.findChild((childModel: any) => {
            counter++;
    
            return childModel.get("level") === 2;
        }) as SomeModel;
    
        assert.equal( counter, 2 );
        assert.ok( lvl2model );
        assert.equal( lvl2model.get("level"), 2 );
    
    });

    it("filterChildren", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    child: SomeModel,
                    level: Types.Number
                };
            }
        }

        const model = new SomeModel({
            level: 0,
            child: {
                level: 1,
                child: {
                    level: 2,
                    child: {
                        level: 3,
                        child: {
                            level: 4
                        }
                    }
                }
            }
        });

        const models = model.filterChildren((childModel: any) =>
            childModel.get("level") % 2  === 0
        ) as SomeModel[];
    
        assert.equal( models.length, 2 );
        assert.equal( models[0].get("level"), 2 );
        assert.equal( models[1].get("level"), 4 );
    
    });


    it("findParent", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    child: SomeModel,
                    level: Types.Number
                };
            }
        }

        const model = new SomeModel({
            level: 0,
            child: {
                level: 1,
                child: {
                    level: 2,
                    child: {
                        level: 3,
                        child: {
                            level: 4
                        }
                    }
                }
            }
        });

        const lastModel = model.findChild((childModel: any) =>
            childModel.get("level") === 4
        ) as SomeModel;
    
        assert.equal( lastModel.get("level"), 4 );
        
        let counter = 0;
        const lvl1 = lastModel.findParent((childModel: any) => {
            counter++;

            return childModel.get("level") === 1;
        }) as SomeModel;

        assert.ok( lvl1 );
        assert.equal( lvl1.get("level"), 1 );
        assert.equal( counter, 3 );

    });


    it("filterParents", () => {

        class SomeModel extends Model<SomeModel> {
            structure() {
                return {
                    child: SomeModel,
                    level: Types.Number
                };
            }
        }

        const model = new SomeModel({
            level: 0,
            child: {
                level: 1,
                child: {
                    level: 2,
                    child: {
                        level: 3,
                        child: {
                            level: 4
                        }
                    }
                }
            }
        });

        const lastModel = model.findChild((childModel: any) =>
            childModel.get("level") === 4
        ) as SomeModel;

        const models = lastModel.filterParents((childModel: any) =>
            childModel.get("level") % 2  === 1
        ) as SomeModel[];
    
        assert.equal( models.length, 2 );
        assert.equal( models[0].get("level"), 3 );
        assert.equal( models[1].get("level"), 1 );
    
    });

    it("findParentInstance", () => {

        class CModel extends Model<CModel> {
            structure() {
                return {
                    name: Types.String
                };
            }
        }

        class BModel extends Model<BModel> {
            structure() {
                return {
                    name: Types.String,
                    child: CModel
                };
            }
        }

        class AModel extends Model<AModel> {
            structure() {
                return {
                    name: Types.String,
                    child: BModel
                };
            }
        }

        const model = new AModel({
            name: "a",
            child: {
                name: "b",
                child: {
                    name: "c"
                }
            }
        });

        const cModel = model.findChild((childModel) =>
            childModel instanceof CModel
        ) as CModel;

        const aModel = cModel.findParentInstance(AModel);
        const bModel = cModel.findParentInstance(BModel);
    
        assert.equal( aModel.get("name"), "a" );
        assert.equal( bModel.get("name"), "b" );
        assert.equal( cModel.get("name"), "c" );
    });



    it("walk by array", () => {
    
        class JobModel extends Model<JobModel> {
            structure() {
                return {
                    name: Types.String
                };
            }
        }
        class UserModel extends Model<UserModel> {
            structure() {
                return {
                    name: Types.String,
                    job: JobModel
                };
            }
        }

        class CompanyModel extends Model<CompanyModel> {
            structure() {
                return {
                    name: Types.String,
                    managers: Types.Array({
                        element: UserModel
                    })
                };
            }
        }

        class OrderModel extends Model<OrderModel> {
            structure() {
                return {
                    client: CompanyModel,
                    partner: CompanyModel
                };
            }
        }

        const partnerCompanyModel = new CompanyModel({
            name: "World Company"
        });

        const orderModel = new OrderModel({
            client: {
                name: "Red Company",
                managers: [
                    {
                        name: "Oliver",
                        job: {
                            name: "Manager"
                        }
                    },
                    {
                        name: "Bob",
                        job: {
                            name: "Director"
                        }
                    }
                ]
            },
            partner: partnerCompanyModel
        });

        const jobModel = orderModel.findChild((childModel) =>
            childModel instanceof JobModel &&
            childModel.get("name") === "Director"
        ) as JobModel;
        assert.equal( jobModel.get("name"), "Director" );

        let walkByArray = false;
        orderModel.walk((childModel) => {
            if ( childModel instanceof JobModel ) {
                walkByArray = true;
            }
        });
        assert.equal(walkByArray, true);


        let filterChildrenByArray = false;
        orderModel.filterChildren((childModel: any) => {
            if ( childModel instanceof JobModel ) {
                filterChildrenByArray = true;
                return true;
            }
            return false;
        });
        assert.equal(filterChildrenByArray, true);
    });

    it("walk by collection", () => {
    
        class Item extends Model<Item> {
            structure() {
                return {
                    name: Types.String,
                    items: Items
                };
            }
        }

        class Items extends Collection<Item> {
            Model() {
                return Item;
            }
        }

        const tree = new Item({
            name: "",
            items: [
                {
                    name: "root",
                    items: [
                        {
                            name: "home",
                            items: [
                                {name: "hello"}
                            ]
                        }
                    ]
                }
            ]
        });

        let results: string[] = [];

        tree.walk((model: any) => {
            results.push(model.get("name"));
        });
        assert.deepStrictEqual(results, ["root", "home", "hello"], "walk");

        results = tree.filterChildrenByInstance(Item)
            .map((item) => item.get("name")!);
        assert.deepStrictEqual(results, ["root", "home", "hello"], "filterChildrenByInstance");
        
        
        results = tree.filterChildren((item: any) => true)
            .map((item) => item.get("name"));
        assert.deepStrictEqual(results, ["root", "home", "hello"], "filterChildren");
    });

    it("filterChildrenByInstance", () => {

        class FileModel extends Model<FileModel> {
            structure() {
                return {
                    name: Types.String
                };
            }
        }

        class FolderModel extends Model<FolderModel> {
            structure(): {
                name: IStringType;
                files: IArrayType<FileModel>;
                folders: IArrayType<FolderModel>;
            } {
                return {
                    name: Types.String,
                    files: Types.Array({
                        element: FileModel
                    }),
                    folders: Types.Array({
                        element: FolderModel
                    })
                };
            }
        }

        const rootFolder = new FolderModel({
            name: "root",
            files: [
                {name: "root-a"},
                {name: "root-b"}
            ],
            folders: [
                {
                    name: "home",
                    files: [
                        {name: "home-a"},
                        {name: "home-b"}
                    ]
                }
            ]
        });

        const files = rootFolder.filterChildrenByInstance(FileModel);
        const filesNames = files.map((file) => file.get("name"));

        assert.deepStrictEqual(filesNames, [
            "root-a", "root-b", 
            "home-a", "home-b"
        ]);
    });



    it("walk by array", () => {
    
        class JobModel extends Model<JobModel> {
            structure() {
                return {
                    name: Types.String
                };
            }
        }
        class UserModel extends Model<UserModel> {
            structure() {
                return {
                    name: Types.String,
                    job: JobModel
                };
            }
        }

        class CompanyModel extends Model<CompanyModel> {
            structure() {
                return {
                    name: Types.String,
                    managers: Types.Array({
                        element: UserModel
                    })
                };
            }
        }

        class OrderModel extends Model<OrderModel> {
            structure() {
                return {
                    client: CompanyModel,
                    partner: CompanyModel
                };
            }
        }

        const partnerCompanyModel = new CompanyModel({
            name: "World Company"
        });

        const orderModel = new OrderModel({
            client: {
                name: "Red Company",
                managers: [
                    {
                        name: "Oliver",
                        job: {
                            name: "Manager"
                        }
                    },
                    {
                        name: "Bob",
                        job: {
                            name: "Director"
                        }
                    }
                ]
            },
            partner: partnerCompanyModel
        });

        const jobModel = orderModel.findChild((childModel) =>
            childModel instanceof JobModel &&
            childModel.get("name") === "Director"
        ) as JobModel;
        assert.equal( jobModel.get("name"), "Director" );

        let walkByArray = false;
        orderModel.walk((childModel) => {
            if ( childModel instanceof JobModel ) {
                walkByArray = true;
            }
        });
        assert.equal(walkByArray, true);


        let filterChildrenByArray = false;
        orderModel.filterChildren((childModel: any) => {
            if ( childModel instanceof JobModel ) {
                filterChildrenByArray = true;
                return true;
            }
            return false;
        });
        assert.equal(filterChildrenByArray, true);
    });


    it("walk by any key and any value", () => {

        class TreeModel extends Model<TreeModel> {
            structure() {
                return {
                    "*": Types.Any
                };
            }
        }

        const lvl4 = new TreeModel({
            lvl: 4
        });

        const lvl3 = new TreeModel({
            lvl: 3,
            children: [lvl4]
        });
        const lvl2 = new TreeModel({
            lvl: 2,
            child: lvl3
        });
        const lvl1 = new TreeModel({
            lvl: 1,
            child: lvl2
        });

        const tmp: number[] = [];
        lvl1.walk((model: any) => {
            tmp.push( model.get("lvl") );
        });

        assert.deepEqual( tmp, [2, 3, 4] );
    });

    it("circular walk", () => {

        class TreeModel extends Model<TreeModel> {
            structure() {
                return {
                    name: Types.String,
                    child: TreeModel
                };
            }
        }

        const root = new TreeModel({
            name: "root"
        });
        const child = new TreeModel({
            name: "child",
            child: root
        });
        root.set({
            child
        });

        const tmp: string[] = [];
        root.walk((model: any) => {
            const name = model.get("name");
            tmp.push( name );
        });

        assert.deepEqual(tmp, [
            "child",
            "root"
        ]);
    });

    it("circular findParent", () => {

        class TreeModel extends Model<TreeModel> {
            structure() {
                return {
                    name: Types.String,
                    child: TreeModel
                };
            }
        }

        const root = new TreeModel({
            name: "root"
        });
        const child = new TreeModel({
            name: "child",
            child: root
        });
        root.set({
            child
        });

        const tmp: string[] = [];
        root.findParent((parentModel: any) => {
            const name = parentModel.get("name");
            tmp.push( name );
            return false;
        });

        assert.deepEqual(tmp, [
            "child",
            "root"
        ]);
    });
    
});
