/*
 * Copyright (C) 2023-2024  Yomitan 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/>.
 */

import Ajv from 'ajv';
import {readFileSync} from 'fs';
import {join, dirname as pathDirname} from 'path';
import {createGenerator} from 'ts-json-schema-generator';
import {fileURLToPath} from 'url';
import {describe, expect, test} from 'vitest';
import {parseJson} from '../dev/json.js';
import {getAllFiles} from '../dev/util.js';

const dirname = pathDirname(fileURLToPath(import.meta.url));
const rootDir = join(dirname, '..');

/**
 * @param {import('test/json').JsconfigType|undefined} jsconfigType
 * @returns {string}
 */
function getJsconfigPath(jsconfigType) {
    let path;
    switch (jsconfigType) {
        case 'dev': path = '../dev/jsconfig.json'; break;
        case 'test': path = '../test/jsconfig.json'; break;
        case 'benches': path = '../benches/jsconfig.json'; break;
        default: path = '../jsconfig.json'; break;
    }
    return join(dirname, path);
}

/**
 * @returns {Ajv}
 */
function createAjv() {
    return new Ajv({
        meta: true,
        strictTuples: false,
        allowUnionTypes: true
    });
}

/**
 * @param {string} path
 * @param {string} type
 * @param {import('test/json').JsconfigType|undefined} jsconfigType
 * @returns {import('ajv').ValidateFunction<unknown>}
 */
function createValidatorFunctionFromTypeScript(path, type, jsconfigType) {
    /** @type {import('ts-json-schema-generator/dist/src/Config').Config} */
    const config = {
        path,
        tsconfig: getJsconfigPath(jsconfigType),
        type,
        jsDoc: 'none',
        additionalProperties: false,
        minify: false,
        expose: 'none',
        strictTuples: true
    };
    const schema = createGenerator(config).createSchema(config.type);
    const ajv = createAjv();
    return ajv.compile(schema);
}

/**
 * @param {string} path
 * @returns {import('ajv').ValidateFunction<unknown>}
 */
function createValidatorFunctionFromSchemaJson(path) {
    /** @type {import('ajv').Schema} */
    const schema = parseJson(readFileSync(path, {encoding: 'utf8'}));
    const ajv = createAjv();
    return ajv.compile(schema);
}

/**
 * @param {string} value
 * @returns {string}
 */
function normalizePathDirectorySeparators(value) {
    return value.replace(/\\/g, '/');
}


describe.concurrent('JSON validation', () => {
    const ignoreDirectories = new Set([
        'builds',
        'dictionaries',
        'node_modules',
        'playwright-report',
        'playwright',
        'test-results',
        'dev/lib',
        'test/playwright'
    ]);

    const existingJsonFiles = getAllFiles(rootDir, (path, isDirectory) => {
        const fileNameNormalized = normalizePathDirectorySeparators(path);
        return (
            isDirectory ?
            !ignoreDirectories.has(fileNameNormalized) :
            /\.json$/i.test(fileNameNormalized)
        );
    });
    /** @type {Set<string>} */
    const existingJsonFileSet = new Set();
    for (const path of existingJsonFiles) {
        existingJsonFileSet.add(normalizePathDirectorySeparators(path));
    }

    const jsonFileName = 'json.json';

    /** @type {import('test/json').JsonInfo} */
    const jsonFileData = parseJson(readFileSync(join(dirname, `data/${jsonFileName}`), {encoding: 'utf8'}));

    test(`Each item in ${jsonFileName} must have a unique path`, () => {
        /** @type {Set<string>} */
        const set = new Set();
        for (const {path} of jsonFileData.files) {
            set.add(path);
        }
        expect(set.size).toBe(jsonFileData.files.length);
    });

    /** @type {Map<string, import('test/json').JsonFileInfo>} */
    const jsonFileMap = new Map();
    for (const item of jsonFileData.files) {
        jsonFileMap.set(item.path, item);
    }

    // Validate file existance
    const requiredFiles = jsonFileData.files.filter((v) => !v.ignore);
    test.each(requiredFiles)('File must exist in project: $path', ({path}) => {
        expect(existingJsonFileSet.has(path)).toBe(true);
    });

    // Validate new files
    const existingJsonFiles2 = existingJsonFiles.map((path) => ({path: normalizePathDirectorySeparators(path)}));
    test.each(existingJsonFiles2)(`File must exist in ${jsonFileName}: $path`, ({path}) => {
        expect(jsonFileMap.has(path)).toBe(true);
    });

    // Validate schemas 1
    /** @type {import('test/json').JsonFileParseInfo[]} */
    const schemaValidationTargets1 = [];
    for (const info of jsonFileData.files) {
        if (info.ignore || !existingJsonFileSet.has(info.path)) { continue; }
        schemaValidationTargets1.push(info);
    }
    test.each(schemaValidationTargets1)('Validating file against TypeScript: $path', ({path, typeFile, type, jsconfig}) => {
        const validate = createValidatorFunctionFromTypeScript(join(rootDir, typeFile), type, jsconfig);
        const data = parseJson(readFileSync(join(rootDir, path), {encoding: 'utf8'}));
        const valid = validate(data);
        const {errors} = validate;
        expect(errors).toBe(null);
        expect(valid).toBe(true);
    });

    // Validate schemas 2
    /** @type {{path: string, schema: string}[]} */
    const schemaValidationTargets2 = [];
    for (const info of jsonFileData.files) {
        if (info.ignore || !existingJsonFileSet.has(info.path)) { continue; }
        const {schema, path} = info;
        if (typeof schema !== 'string') { continue; }
        schemaValidationTargets2.push({schema, path});
    }
    test.each(schemaValidationTargets2)('Validating file against schema: $path', ({path, schema}) => {
        const validate = createValidatorFunctionFromSchemaJson(join(rootDir, schema));
        const data = parseJson(readFileSync(join(rootDir, path), {encoding: 'utf8'}));
        const valid = validate(data);
        const {errors} = validate;
        expect(errors).toBe(null);
        expect(valid).toBe(true);
    });
});