/*
 * Copyright (C) 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 esbuild from 'esbuild';
import {readFileSync} from 'fs';
import {dirname, join} from 'path';
import {fileURLToPath} from 'url';
import {describe, test} from 'vitest';
import {parseJson} from '../dev/json.js';

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

/**
 * @param {string[]} scriptPaths
 * @returns {Promise<string[]>}
 */
async function getDependencies(scriptPaths) {
    const v = await esbuild.build({
        entryPoints: scriptPaths,
        bundle: true,
        minify: false,
        sourcemap: true,
        target: 'es2022',
        format: 'esm',
        write: false,
        metafile: true
    });
    const dependencies = Object.keys(v.metafile.inputs);
    const stringComparer = new Intl.Collator('en-US'); // Invariant locale
    dependencies.sort((a, b) => stringComparer.compare(a, b));
    return dependencies;
}

/**
 * @param {string[]} dependencies
 * @returns {string[]}
 */
function removeLibraryDependencies(dependencies) {
    const pattern = /^ext\/lib\//;
    return dependencies.filter((v) => !pattern.test(v));
}

/**
 * @param {import('test/eslint-config').MinimalEslintConfigEnv|undefined} env1
 * @param {import('test/eslint-config').MinimalEslintConfigEnv} env2
 * @returns {boolean}
 */
function envMatches(env1, env2) {
    if (typeof env1 !== 'object' || env1 === null) { return false; }
    const map1 = new Map(Object.entries(env1));
    const map2 = new Map(Object.entries(env2));
    if (map1.size !== map2.size) { return false; }
    for (const [k1, v1] of map1) {
        if (map2.get(k1) !== v1) { return false; }
    }
    return true;
}

/**
 * @param {string[]} files1
 * @param {string[]} files2
 * @returns {boolean}
 */
function filesMatch(files1, files2) {
    if (!Array.isArray(files1)) { return false; }
    const set1 = new Set(files1);
    const set2 = new Set(files2);
    if (set1.size !== set2.size) { return false; }
    for (const v of set1) {
        if (!set2.has(v)) { return false; }
    }
    return true;
}

const targets = [
    {
        name: 'sandbox',
        paths: [
            'ext/js/templates/template-renderer-frame-main.js'
        ],
        /** @type {import('test/eslint-config').MinimalEslintConfigEnv} */
        env: {
            webextensions: false
        }
    },
    {
        name: 'worker',
        paths: [
            'ext/js/dictionary/dictionary-worker-main.js'
        ],
        /** @type {import('test/eslint-config').MinimalEslintConfigEnv} */
        env: {
            browser: false,
            worker: true
        }
    },
    {
        name: 'serviceworker',
        paths: [
            'ext/js/background/background-main.js'
        ],
        /** @type {import('test/eslint-config').MinimalEslintConfigEnv} */
        env: {
            browser: false,
            serviceworker: true
        }
    }
];

describe('Eslint configuration', () => {
    const eslintConfigPath = '.eslintrc.json';
    /** @type {import('test/eslint-config').MinimalEslintConfig} */
    const eslintConfig = parseJson(readFileSync(join(rootDir, eslintConfigPath), 'utf8'));
    describe.each(targets)('Environment is $name', ({name, paths, env}) => {
        test('Entry exists', async ({expect}) => {
            const fullPaths = paths.map((v) => join(rootDir, v));
            const dependencies = removeLibraryDependencies(await getDependencies(fullPaths));

            let okay = false;
            const candidates = [];
            const {overrides} = eslintConfig;
            for (let i = 0, ii = overrides.length; i < ii; ++i) {
                const override = overrides[i];
                if (!envMatches(override.env, env)) { continue; }
                const {files} = override;
                if (!Array.isArray(files)) { continue; }
                candidates.push(i);
                if (filesMatch(files, dependencies)) {
                    okay = true;
                    break;
                }
            }

            if (okay) { return; }
            switch (candidates.length) {
                case 0:
                    {
                        const message = `No override found with "${name}" environment: ${JSON.stringify(env)}`;
                        expect(false, message).toStrictEqual(true);
                    }
                    break;
                case 1:
                    {
                        const index = candidates[0];
                        const message = `Override at index ${index} does not have the expected files for the "${name}" environment.`;
                        expect(overrides[index].files, message).toStrictEqual(dependencies);
                    }
                    break;
                default:
                    {
                        const message = `No override found with the correct file list for the "${name}" environment; candidate indices: [${candidates.join(', ')}].`;
                        expect([], message).toStrictEqual(dependencies);
                    }
                    break;
            }
        });
    });
});