/*
 * Copyright (C) 2020-2021  Yomichan Authors
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

const assert = require('assert');
const {testMain} = require('../dev/util');
const {VM} = require('../dev/vm');


const vm = new VM({});
vm.execute([
    'mixed/js/core.js',
    'mixed/js/cache-map.js',
    'bg/js/json-schema.js',
    'bg/js/profile-conditions.js'
]);
const [JsonSchemaValidator, ProfileConditions] = vm.get(['JsonSchemaValidator', 'ProfileConditions']);


function schemaValidate(value, schema) {
    return new JsonSchemaValidator().isValid(value, schema);
}


function testNormalizeContext() {
    const data = [
        // Empty
        {
            context: {},
            expected: {}
        },

        // Domain normalization
        {
            context: {url: ''},
            expected: {url: ''}
        },
        {
            context: {url: 'http://example.com/'},
            expected: {url: 'http://example.com/', domain: 'example.com'}
        },
        {
            context: {url: 'http://example.com:1234/'},
            expected: {url: 'http://example.com:1234/', domain: 'example.com'}
        },
        {
            context: {url: 'http://user@example.com:1234/'},
            expected: {url: 'http://user@example.com:1234/', domain: 'example.com'}
        }
    ];

    for (const {context, expected} of data) {
        const profileConditions = new ProfileConditions();
        const actual = profileConditions.normalizeContext(context);
        vm.assert.deepStrictEqual(actual, expected);
    }
}

function testSchemas() {
    const data = [
        // Empty
        {
            conditionGroups: [],
            expectedSchema: {},
            inputs: [
                {expected: true, context: {url: 'http://example.com/'}}
            ]
        },
        {
            conditionGroups: [
                {conditions: []}
            ],
            expectedSchema: {},
            inputs: [
                {expected: true, context: {url: 'http://example.com/'}}
            ]
        },
        {
            conditionGroups: [
                {conditions: []},
                {conditions: []}
            ],
            expectedSchema: {},
            inputs: [
                {expected: true, context: {url: 'http://example.com/'}}
            ]
        },

        // popupLevel tests
        {
            conditionGroups: [
                {
                    conditions: [
                        {
                            type: 'popupLevel',
                            operator: 'equal',
                            value: '0'
                        }
                    ]
                }
            ],
            expectedSchema: {
                properties: {
                    depth: {const: 0}
                },
                required: ['depth']
            },
            inputs: [
                {expected: true,  context: {depth: 0, url: 'http://example.com/'}},
                {expected: false, context: {depth: 1, url: 'http://example.com/'}},
                {expected: false, context: {depth: -1, url: 'http://example.com/'}}
            ]
        },
        {
            conditionGroups: [
                {
                    conditions: [
                        {
                            type: 'popupLevel',
                            operator: 'notEqual',
                            value: '0'
                        }
                    ]
                }
            ],
            expectedSchema: {
                not: [
                    {
                        properties: {
                            depth: {const: 0}
                        },
                        required: ['depth']
                    }
                ]
            },
            inputs: [
                {expected: false, context: {depth: 0, url: 'http://example.com/'}},
                {expected: true,  context: {depth: 1, url: 'http://example.com/'}},
                {expected: true,  context: {depth: -1, url: 'http://example.com/'}}
            ]
        },
        {
            conditionGroups: [
                {
                    conditions: [
                        {
                            type: 'popupLevel',
                            operator: 'lessThan',
                            value: '0'
                        }
                    ]
                }
            ],
            expectedSchema: {
                properties: {
                    depth: {
                        type: 'number',
                        exclusiveMaximum: 0
                    }
                },
                required: ['depth']
            },
            inputs: [
                {expected: false, context: {depth: 0, url: 'http://example.com/'}},
                {expected: false, context: {depth: 1, url: 'http://example.com/'}},
                {expected: true,  context: {depth: -1, url: 'http://example.com/'}}
            ]
        },
        {
            conditionGroups: [
                {
                    conditions: [
                        {
                            type: 'popupLevel',
                            operator: 'greaterThan',
                            value: '0'
                        }
                    ]
                }
            ],
            expectedSchema: {
                properties: {
                    depth: {
                        type: 'number',
                        exclusiveMinimum: 0
                    }
                },
                required: ['depth']
            },
            inputs: [
                {expected: false, context: {depth: 0, url: 'http://example.com/'}},
                {expected: true,  context: {depth: 1, url: 'http://example.com/'}},
                {expected: false, context: {depth: -1, url: 'http://example.com/'}}
            ]
        },
        {
            conditionGroups: [
                {
                    conditions: [
                        {
                            type: 'popupLevel',
                            operator: 'lessThanOrEqual',
                            value: '0'
                        }
                    ]
                }
            ],
            expectedSchema: {
                properties: {
                    depth: {
                        type: 'number',
                        maximum: 0
                    }
                },
                required: ['depth']
            },
            inputs: [
                {expected: true,  context: {depth: 0, url: 'http://example.com/'}},
                {expected: false, context: {depth: 1, url: 'http://example.com/'}},
                {expected: true,  context: {depth: -1, url: 'http://example.com/'}}
            ]
        },
        {
            conditionGroups: [
                {
                    conditions: [
                        {
                            type: 'popupLevel',
                            operator: 'greaterThanOrEqual',
                            value: '0'
                        }
                    ]
                }
            ],
            expectedSchema: {
                properties: {
                    depth: {
                        type: 'number',
                        minimum: 0
                    }
                },
                required: ['depth']
            },
            inputs: [
                {expected: true,  context: {depth: 0, url: 'http://example.com/'}},
                {expected: true,  context: {depth: 1, url: 'http://example.com/'}},
                {expected: false, context: {depth: -1, url: 'http://example.com/'}}
            ]
        },

        // url tests
        {
            conditionGroups: [
                {
                    conditions: [
                        {
                            type: 'url',
                            operator: 'matchDomain',
                            value: 'example.com'
                        }
                    ]
                }
            ],
            expectedSchema: {
                properties: {
                    domain: {
                        oneOf: [
                            {const: 'example.com'}
                        ]
                    }
                },
                required: ['domain']
            },
            inputs: [
                {expected: true,  context: {depth: 0, url: 'http://example.com/'}},
                {expected: false, context: {depth: 0, url: 'http://example1.com/'}},
                {expected: false, context: {depth: 0, url: 'http://example2.com/'}},
                {expected: true,  context: {depth: 0, url: 'http://example.com:1234/'}},
                {expected: true,  context: {depth: 0, url: 'http://user@example.com:1234/'}}
            ]
        },
        {
            conditionGroups: [
                {
                    conditions: [
                        {
                            type: 'url',
                            operator: 'matchDomain',
                            value: 'example.com, example1.com, example2.com'
                        }
                    ]
                }
            ],
            expectedSchema: {
                properties: {
                    domain: {
                        oneOf: [
                            {const: 'example.com'},
                            {const: 'example1.com'},
                            {const: 'example2.com'}
                        ]
                    }
                },
                required: ['domain']
            },
            inputs: [
                {expected: true,  context: {depth: 0, url: 'http://example.com/'}},
                {expected: true,  context: {depth: 0, url: 'http://example1.com/'}},
                {expected: true,  context: {depth: 0, url: 'http://example2.com/'}},
                {expected: false, context: {depth: 0, url: 'http://example3.com/'}},
                {expected: true,  context: {depth: 0, url: 'http://example.com:1234/'}},
                {expected: true,  context: {depth: 0, url: 'http://user@example.com:1234/'}}
            ]
        },
        {
            conditionGroups: [
                {
                    conditions: [
                        {
                            type: 'url',
                            operator: 'matchRegExp',
                            value: '^http://example\\d?\\.com/[\\w\\W]*$'
                        }
                    ]
                }
            ],
            expectedSchema: {
                properties: {
                    url: {
                        type: 'string',
                        pattern: '^http://example\\d?\\.com/[\\w\\W]*$',
                        patternFlags: 'i'
                    }
                },
                required: ['url']
            },
            inputs: [
                {expected: true,  context: {depth: 0, url: 'http://example.com/'}},
                {expected: true,  context: {depth: 0, url: 'http://example1.com/'}},
                {expected: true,  context: {depth: 0, url: 'http://example2.com/'}},
                {expected: true,  context: {depth: 0, url: 'http://example3.com/'}},
                {expected: true,  context: {depth: 0, url: 'http://example.com/example'}},
                {expected: false, context: {depth: 0, url: 'http://example.com:1234/'}},
                {expected: false, context: {depth: 0, url: 'http://user@example.com:1234/'}},
                {expected: false, context: {depth: 0, url: 'http://example-1.com/'}}
            ]
        },

        // modifierKeys tests
        {
            conditionGroups: [
                {
                    conditions: [
                        {
                            type: 'modifierKeys',
                            operator: 'are',
                            value: ''
                        }
                    ]
                }
            ],
            expectedSchema: {
                properties: {
                    modifierKeys: {
                        type: 'array',
                        maxItems: 0,
                        minItems: 0
                    }
                },
                required: ['modifierKeys']
            },
            inputs: [
                {expected: true,  context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
                {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
                {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
                {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
            ]
        },
        {
            conditionGroups: [
                {
                    conditions: [
                        {
                            type: 'modifierKeys',
                            operator: 'are',
                            value: 'Alt, Shift'
                        }
                    ]
                }
            ],
            expectedSchema: {
                properties: {
                    modifierKeys: {
                        type: 'array',
                        maxItems: 2,
                        minItems: 2,
                        allOf: [
                            {contains: {const: 'Alt'}},
                            {contains: {const: 'Shift'}}
                        ]
                    }
                },
                required: ['modifierKeys']
            },
            inputs: [
                {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
                {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
                {expected: true,  context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
                {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
            ]
        },
        {
            conditionGroups: [
                {
                    conditions: [
                        {
                            type: 'modifierKeys',
                            operator: 'areNot',
                            value: ''
                        }
                    ]
                }
            ],
            expectedSchema: {
                not: [
                    {
                        properties: {
                            modifierKeys: {
                                type: 'array',
                                maxItems: 0,
                                minItems: 0
                            }
                        },
                        required: ['modifierKeys']
                    }
                ]
            },
            inputs: [
                {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
                {expected: true,  context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
                {expected: true,  context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
                {expected: true,  context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
            ]
        },
        {
            conditionGroups: [
                {
                    conditions: [
                        {
                            type: 'modifierKeys',
                            operator: 'areNot',
                            value: 'Alt, Shift'
                        }
                    ]
                }
            ],
            expectedSchema: {
                not: [
                    {
                        properties: {
                            modifierKeys: {
                                type: 'array',
                                maxItems: 2,
                                minItems: 2,
                                allOf: [
                                    {contains: {const: 'Alt'}},
                                    {contains: {const: 'Shift'}}
                                ]
                            }
                        },
                        required: ['modifierKeys']
                    }
                ]
            },
            inputs: [
                {expected: true,  context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
                {expected: true,  context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
                {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
                {expected: true,  context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
            ]
        },
        {
            conditionGroups: [
                {
                    conditions: [
                        {
                            type: 'modifierKeys',
                            operator: 'include',
                            value: ''
                        }
                    ]
                }
            ],
            expectedSchema: {
                properties: {
                    modifierKeys: {
                        type: 'array',
                        minItems: 0
                    }
                },
                required: ['modifierKeys']
            },
            inputs: [
                {expected: true,  context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
                {expected: true,  context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
                {expected: true,  context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
                {expected: true,  context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
            ]
        },
        {
            conditionGroups: [
                {
                    conditions: [
                        {
                            type: 'modifierKeys',
                            operator: 'include',
                            value: 'Alt, Shift'
                        }
                    ]
                }
            ],
            expectedSchema: {
                properties: {
                    modifierKeys: {
                        type: 'array',
                        minItems: 2,
                        allOf: [
                            {contains: {const: 'Alt'}},
                            {contains: {const: 'Shift'}}
                        ]
                    }
                },
                required: ['modifierKeys']
            },
            inputs: [
                {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
                {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
                {expected: true,  context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
                {expected: true,  context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
            ]
        },
        {
            conditionGroups: [
                {
                    conditions: [
                        {
                            type: 'modifierKeys',
                            operator: 'notInclude',
                            value: ''
                        }
                    ]
                }
            ],
            expectedSchema: {
                properties: {
                    modifierKeys: {
                        type: 'array'
                    }
                },
                required: ['modifierKeys']
            },
            inputs: [
                {expected: true,  context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
                {expected: true,  context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
                {expected: true,  context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
                {expected: true,  context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
            ]
        },
        {
            conditionGroups: [
                {
                    conditions: [
                        {
                            type: 'modifierKeys',
                            operator: 'notInclude',
                            value: 'Alt, Shift'
                        }
                    ]
                }
            ],
            expectedSchema: {
                properties: {
                    modifierKeys: {
                        type: 'array',
                        not: [
                            {contains: {const: 'Alt'}},
                            {contains: {const: 'Shift'}}
                        ]
                    }
                },
                required: ['modifierKeys']
            },
            inputs: [
                {expected: true,  context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
                {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
                {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
                {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
            ]
        },

        // Multiple conditions tests
        {
            conditionGroups: [
                {
                    conditions: [
                        {
                            type: 'popupLevel',
                            operator: 'greaterThan',
                            value: '0'
                        },
                        {
                            type: 'popupLevel',
                            operator: 'lessThan',
                            value: '3'
                        }
                    ]
                }
            ],
            expectedSchema: {
                allOf: [
                    {
                        properties: {
                            depth: {
                                type: 'number',
                                exclusiveMinimum: 0
                            }
                        },
                        required: ['depth']
                    },
                    {
                        properties: {
                            depth: {
                                type: 'number',
                                exclusiveMaximum: 3
                            }
                        },
                        required: ['depth']
                    }
                ]
            },
            inputs: [
                {expected: false, context: {depth: -2, url: 'http://example.com/'}},
                {expected: false, context: {depth: -1, url: 'http://example.com/'}},
                {expected: false, context: {depth: 0, url: 'http://example.com/'}},
                {expected: true,  context: {depth: 1, url: 'http://example.com/'}},
                {expected: true,  context: {depth: 2, url: 'http://example.com/'}},
                {expected: false, context: {depth: 3, url: 'http://example.com/'}}
            ]
        },
        {
            conditionGroups: [
                {
                    conditions: [
                        {
                            type: 'popupLevel',
                            operator: 'greaterThan',
                            value: '0'
                        },
                        {
                            type: 'popupLevel',
                            operator: 'lessThan',
                            value: '3'
                        }
                    ]
                },
                {
                    conditions: [
                        {
                            type: 'popupLevel',
                            operator: 'equal',
                            value: '0'
                        }
                    ]
                }
            ],
            expectedSchema: {
                anyOf: [
                    {
                        allOf: [
                            {
                                properties: {
                                    depth: {
                                        type: 'number',
                                        exclusiveMinimum: 0
                                    }
                                },
                                required: ['depth']
                            },
                            {
                                properties: {
                                    depth: {
                                        type: 'number',
                                        exclusiveMaximum: 3
                                    }
                                },
                                required: ['depth']
                            }
                        ]
                    },
                    {
                        properties: {
                            depth: {const: 0}
                        },
                        required: ['depth']
                    }
                ]
            },
            inputs: [
                {expected: false, context: {depth: -2, url: 'http://example.com/'}},
                {expected: false, context: {depth: -1, url: 'http://example.com/'}},
                {expected: true,  context: {depth: 0, url: 'http://example.com/'}},
                {expected: true,  context: {depth: 1, url: 'http://example.com/'}},
                {expected: true,  context: {depth: 2, url: 'http://example.com/'}},
                {expected: false, context: {depth: 3, url: 'http://example.com/'}}
            ]
        },
        {
            conditionGroups: [
                {
                    conditions: [
                        {
                            type: 'popupLevel',
                            operator: 'greaterThan',
                            value: '0'
                        },
                        {
                            type: 'popupLevel',
                            operator: 'lessThan',
                            value: '3'
                        }
                    ]
                },
                {
                    conditions: [
                        {
                            type: 'popupLevel',
                            operator: 'lessThanOrEqual',
                            value: '0'
                        },
                        {
                            type: 'popupLevel',
                            operator: 'greaterThanOrEqual',
                            value: '-1'
                        }
                    ]
                }
            ],
            expectedSchema: {
                anyOf: [
                    {
                        allOf: [
                            {
                                properties: {
                                    depth: {
                                        type: 'number',
                                        exclusiveMinimum: 0
                                    }
                                },
                                required: ['depth']
                            },
                            {
                                properties: {
                                    depth: {
                                        type: 'number',
                                        exclusiveMaximum: 3
                                    }
                                },
                                required: ['depth']
                            }
                        ]
                    },
                    {
                        allOf: [
                            {
                                properties: {
                                    depth: {
                                        type: 'number',
                                        maximum: 0
                                    }
                                },
                                required: ['depth']
                            },
                            {
                                properties: {
                                    depth: {
                                        type: 'number',
                                        minimum: -1
                                    }
                                },
                                required: ['depth']
                            }
                        ]
                    }
                ]
            },
            inputs: [
                {expected: false, context: {depth: -2, url: 'http://example.com/'}},
                {expected: true,  context: {depth: -1, url: 'http://example.com/'}},
                {expected: true,  context: {depth: 0, url: 'http://example.com/'}},
                {expected: true,  context: {depth: 1, url: 'http://example.com/'}},
                {expected: true,  context: {depth: 2, url: 'http://example.com/'}},
                {expected: false, context: {depth: 3, url: 'http://example.com/'}}
            ]
        }
    ];

    for (const {conditionGroups, expectedSchema, inputs} of data) {
        const profileConditions = new ProfileConditions();
        const schema = profileConditions.createSchema(conditionGroups);
        if (typeof expectedSchema !== 'undefined') {
            vm.assert.deepStrictEqual(schema, expectedSchema);
        }
        if (Array.isArray(inputs)) {
            for (const {expected, context} of inputs) {
                const normalizedContext = profileConditions.normalizeContext(context);
                const actual = schemaValidate(normalizedContext, schema);
                assert.strictEqual(actual, expected);
            }
        }
    }
}


function main() {
    testNormalizeContext();
    testSchemas();
}


if (require.main === module) { testMain(main); }