// Launch script for various Node test configurations
import fs from 'fs';
import {resolve} from 'path';
import {execShellCommand} from './utils/utils.js';

import {getOcularConfig} from './helpers/get-ocular-config.js';

import {BrowserTestDriver} from '@probe.gl/test-utils';
import {createServer} from 'vite';

const mode = process.argv[2] ?? 'unknown';
const source = process.argv[3] === 'dist' ? 'dist' : 'src';
const ocularConfig = await getOcularConfig({aliasMode: source});
const viteConfigPath = ocularConfig.vite.configPath;
// c8 default directory for coverage data
const CoverageTempDir = './coverage/tmp';

console.log(`Running ${mode} tests...`); // eslint-disable-line

switch (mode) {
  case 'cover':
    if (ocularConfig.coverage.test === 'node') {
      runNodeTest(resolveNodeEntry('test'), 'npx c8 --reporter=none');
    } else {
      await runBrowserTest({
        server: {
          start: createViteServer,
          options: {
            mode: 'test'
          }
        },
        url: resolveBrowserEntry('test'),
        headless: 'new',
        onStart: async ({page}) => {
          clearCoverage();
          await page.coverage.startJSCoverage({includeRawScriptCoverage: true});
        },
        onFinish: async ({page, isSuccessful}) => {
          const coverage = await page.coverage.stopJSCoverage();
          if (!isSuccessful) return;
          writeCoverage(coverage);
        }
      });
    }
    break;

  case 'node-debug':
    runNodeTest(resolveNodeEntry('test'), '', {breakAndInspect: true});
    break;

  case 'node':
  case 'dist':
    runNodeTest(resolveNodeEntry('test')); // Run the tests
    break;

  case 'browser':
  case 'browser-headless':
    await runBrowserTest({
      server: {
        start: createViteServer,
        options: {
          mode: 'test'
        }
      },
      url: resolveBrowserEntry('test'),
      headless: mode === 'browser-headless' ? 'new' : false
    });
    break;

  default:
    if (/\bbrowser\b/.test(mode)) {
      const testMode = mode.replace('-browser', '').replace('-headless', '');
      await runBrowserTest({
        server: {
          start: createViteServer,
          options: {
            mode: testMode
          }
        },
        url: resolveBrowserEntry(testMode),
        headless: /\bheadless\b/.test(mode) ? 'new' : false
      });
    } else if (mode in ocularConfig.entry) {
      runNodeTest(resolveNodeEntry(mode));
    } else {
      throw new Error(`Unknown test mode ${mode}`);
    }
}

function resolveNodeEntry(key: string): string {
  const entry = ocularConfig.entry[key];
  if (typeof entry === 'string') {
    return resolve(entry);
  }
  throw new Error(`Cannot find entry point ${key} in ocular config.`);
}

function resolveBrowserEntry(key: string): string {
  const fileName = ocularConfig.entry[`${key}-browser`];
  if (typeof fileName === 'string' && fileName.endsWith('.html')) {
    return fileName;
  } else if (fileName) {
    return 'index.html';
  }
  throw new Error(`Cannot find entry point ${key}-browser in ocular config.`);
}

function runNodeTest(
  entry: string,
  command: string = '',
  {breakAndInspect}: {breakAndInspect: boolean} = {breakAndInspect: false}
) {
  // Save module alias
  fs.writeFileSync(
    resolve(ocularConfig.ocularPath, '.alias.json'),
    JSON.stringify(ocularConfig.aliases)
  );

  const inspectBrk = breakAndInspect ? '--inspect-brk' : '';

  if (ocularConfig.esm) {
    execShellCommand(
      `${command} node ${inspectBrk} --import "${ocularConfig.ocularPath}/dist/helpers/esm-register.js" --es-module-specifier-resolution=node "${entry}"`
    );
  } else {
    execShellCommand(
      `${command} ts-node -r "${ocularConfig.ocularPath}/dist/helpers/cjs-register.cjs" "${entry}"`
    );
  }
}

function runBrowserTest(opts) {
  const userConfig = ocularConfig.browserTest || {};
  return new BrowserTestDriver().run({
    ...opts,
    ...userConfig,
    server: {...opts.server, ...userConfig.server},
    browser: {...opts.browser, ...userConfig.browser}
  });
}

async function createViteServer(config) {
  const server = await createServer({
    configFile: viteConfigPath,
    mode: config.options?.mode,
    server: {
      port: config.port
    }
  });
  await server.listen();

  return {
    url: server.resolvedUrls?.local[0],
    stop: () => {
      server.close();
    }
  };
}

function clearCoverage() {
  fs.rmSync(CoverageTempDir, {force: true, recursive: true});
  fs.mkdirSync(CoverageTempDir, {recursive: true});
}

/** Write raw coverage data to disk for c8 to report */
function writeCoverage(coverage) {
  const outputFile = `${CoverageTempDir}/coverage-${Date.now()}`;

  // Convert Chrome coverage format to v8
  let idx = 0;
  for (const cov of coverage) {
    const it = cov.rawScriptCoverage;
    const filePath = it.url.replace(/^http:\/\/localhost:\d+\//, '');

    // Excluded directories
    if (filePath.match(/(^|\/)(node_modules|test|@vite)\//)) continue;
    // Remap file url to path on local disk
    const fileUrl = `file://${resolve(filePath)}`;
    it.url = fileUrl;

    const sourcemapCache = {};
    const [generatedSource, sourcemapDataUrl] = cov.text.split(/\/\/# sourceMappingURL=/);
    if (sourcemapDataUrl) {
      // Save source mapping for c8 reporter
      sourcemapCache[fileUrl] = {
        lineLengths: generatedSource.split('\n').map((l) => l.length),
        data: sourcemapFromDataUrl(sourcemapDataUrl)
      };
    }

    fs.writeFileSync(
      `${outputFile}-${idx++}.json`,
      JSON.stringify({
        result: [it],
        'source-map-cache': sourcemapCache
      }),
      'utf8'
    );
  }
}

function sourcemapFromDataUrl(url: string): string | null {
  const [format, data] = url.split(',');
  const base64 = format.endsWith('base64');
  const decodedData = base64 ? Buffer.from(data, 'base64').toString('utf8') : data;
  try {
    return JSON.parse(decodedData);
  } catch (err) {
    return null;
  }
}
