/* eslint-disable no-console, n/no-restricted-import */

import * as fs from 'fs';
import * as http from 'http';
import { AddressInfo } from 'net';

import { dataCache } from '../framework/data_cache.js';
import { getResourcePath, setBaseResourcePath } from '../framework/resources.js';
import { globalTestConfig } from '../framework/test_config.js';
import { DefaultTestFileLoader } from '../internal/file_loader.js';
import { prettyPrintLog } from '../internal/logging/log_message.js';
import { Logger } from '../internal/logging/logger.js';
import { LiveTestCaseResult, Status } from '../internal/logging/result.js';
import { parseQuery } from '../internal/query/parseQuery.js';
import { TestQueryWithExpectation } from '../internal/query/query.js';
import { TestTreeLeaf } from '../internal/tree.js';
import { Colors } from '../util/colors.js';
import { setDefaultRequestAdapterOptions, setGPUProvider } from '../util/navigator_gpu.js';

import sys from './helper/sys.js';

function usage(rc: number): never {
  console.log(`Usage:
  tools/server [OPTIONS...]
Options:
  --colors                  Enable ANSI colors in output.
  --compat                  Run tests in compatibility mode.
  --coverage                Add coverage data to each result.
  --verbose                 Print result/log of every test as it runs.
  --debug                   Include debug messages in logging.
  --gpu-provider            Path to node module that provides the GPU implementation.
  --gpu-provider-flag       Flag to set on the gpu-provider as <flag>=<value>
  --unroll-const-eval-loops Unrolls loops in constant-evaluation shader execution tests
  --enforce-default-limits  Enforce the default limits (note: powerPreference tests may fail)
  --force-fallback-adapter  Force a fallback adapter
  --log-to-websocket        Log to a websocket
  --u                       Flag to set on the gpu-provider as <flag>=<value>

Provides an HTTP server used for running tests via an HTTP RPC interface
First, load some tree or subtree of tests:
  http://localhost:port/load?unittests:basic:*
To run a single test case, perform an HTTP GET or POST at the URL:
  http://localhost:port/run?unittests:basic:test,sync
To shutdown the server perform an HTTP GET or POST at the URL:
  http://localhost:port/terminate
`);
  return sys.exit(rc);
}

interface RunResult {
  // The result of the test
  status: Status;
  // Any additional messages printed
  message: string;
  // The time it took to execute the test
  durationMS: number;
  // Code coverage data, if the server was started with `--coverage`
  // This data is opaque (implementation defined).
  coverageData?: string;
}

// The interface that exposes creation of the GPU, and optional interface to code coverage.
interface GPUProviderModule {
  // @returns a GPU with the given flags
  create(flags: string[]): GPU;
  // An optional interface to a CodeCoverageProvider
  coverage?: CodeCoverageProvider;
}

interface CodeCoverageProvider {
  // Starts collecting code coverage
  begin(): void;
  // Ends collecting of code coverage, returning the coverage data.
  // This data is opaque (implementation defined).
  end(): string;
}

if (!sys.existsSync('src/common/runtime/cmdline.ts')) {
  console.log('Must be run from repository root');
  usage(1);
}
setBaseResourcePath('out-node/resources');

Colors.enabled = false;

let emitCoverage = false;
let verbose = false;
let gpuProviderModule: GPUProviderModule | undefined = undefined;

const gpuProviderFlags: string[] = [];
for (let i = 0; i < sys.args.length; ++i) {
  const a = sys.args[i];
  if (a.startsWith('-')) {
    if (a === '--colors') {
      Colors.enabled = true;
    } else if (a === '--compat') {
      globalTestConfig.compatibility = true;
    } else if (a === '--coverage') {
      emitCoverage = true;
    } else if (a === '--force-fallback-adapter') {
      globalTestConfig.forceFallbackAdapter = true;
    } else if (a === '--enforce-default-limits') {
      globalTestConfig.enforceDefaultLimits = true;
    } else if (a === '--block-all-features') {
      globalTestConfig.blockAllFeatures = true;
    } else if (a === '--subcases-between-attempting-gc') {
      globalTestConfig.subcasesBetweenAttemptingGC = Number(sys.args[++i]);
    } else if (a === '--cases-between-replacing-device') {
      globalTestConfig.casesBetweenReplacingDevice = Number(sys.args[++i]);
    } else if (a === '--log-to-websocket') {
      globalTestConfig.logToWebSocket = true;
    } else if (a === '--gpu-provider') {
      const modulePath = sys.args[++i];
      gpuProviderModule = require(modulePath);
    } else if (a === '--gpu-provider-flag') {
      gpuProviderFlags.push(sys.args[++i]);
    } else if (a === '--debug') {
      globalTestConfig.enableDebugLogs = true;
    } else if (a === '--unroll-const-eval-loops') {
      globalTestConfig.unrollConstEvalLoops = true;
    } else if (a === '--help') {
      usage(1);
    } else if (a === '--verbose') {
      verbose = true;
    } else {
      console.log(`unrecognized flag: ${a}`);
    }
  }
}

let codeCoverage: CodeCoverageProvider | undefined = undefined;

if (globalTestConfig.compatibility || globalTestConfig.forceFallbackAdapter) {
  setDefaultRequestAdapterOptions({
    featureLevel: globalTestConfig.compatibility ? 'compatibility' : 'core',
    forceFallbackAdapter: globalTestConfig.forceFallbackAdapter,
  });
}

if (gpuProviderModule) {
  setGPUProvider(() => gpuProviderModule!.create(gpuProviderFlags));

  if (emitCoverage) {
    codeCoverage = gpuProviderModule.coverage;
    if (codeCoverage === undefined) {
      console.error(
        `--coverage specified, but the GPUProviderModule does not support code coverage.
Did you remember to build with code coverage instrumentation enabled?`
      );
      sys.exit(1);
    }
  }
}

dataCache.setStore({
  load: (path: string) => {
    return new Promise<Uint8Array>((resolve, reject) => {
      fs.readFile(getResourcePath(`cache/${path}`), (err, data) => {
        if (err !== null) {
          reject(err.message);
        } else {
          resolve(data);
        }
      });
    });
  },
});

if (verbose) {
  dataCache.setDebugLogger(console.log);
}

// eslint-disable-next-line @typescript-eslint/require-await
(async () => {
  const log = new Logger();
  const testcases = new Map<string, TestTreeLeaf>();

  async function runTestcase(
    testcase: TestTreeLeaf,
    expectations: TestQueryWithExpectation[] = []
  ): Promise<LiveTestCaseResult> {
    const name = testcase.query.toString();
    const [rec, res] = log.record(name);
    await testcase.run(rec, expectations);
    return res;
  }

  const server = http.createServer(
    async (request: http.IncomingMessage, response: http.ServerResponse) => {
      if (request.url === undefined) {
        response.end('invalid url');
        return;
      }

      const loadCasesPrefix = '/load?';
      const runPrefix = '/run?';
      const terminatePrefix = '/terminate';

      if (request.url.startsWith(loadCasesPrefix)) {
        const query = request.url.substr(loadCasesPrefix.length);
        try {
          const webgpuQuery = parseQuery(query);
          const loader = new DefaultTestFileLoader();
          for (const testcase of await loader.loadCases(webgpuQuery)) {
            testcases.set(testcase.query.toString(), testcase);
          }
          response.statusCode = 200;
          response.end();
        } catch (err) {
          response.statusCode = 500;
          response.end(`load failed with error: ${err}\n${(err as Error).stack}`);
        }
      } else if (request.url.startsWith(runPrefix)) {
        const name = request.url.substr(runPrefix.length);
        try {
          const testcase = testcases.get(name);
          if (testcase) {
            if (codeCoverage !== undefined) {
              codeCoverage.begin();
            }
            const start = performance.now();
            const result = await runTestcase(testcase);
            const durationMS = performance.now() - start;
            const coverageData = codeCoverage !== undefined ? codeCoverage.end() : undefined;
            let message = '';
            if (result.logs !== undefined) {
              message = result.logs.map(log => prettyPrintLog(log)).join('\n');
            }
            const status = result.status;
            const res: RunResult = { status, message, durationMS, coverageData };
            response.statusCode = 200;
            response.end(JSON.stringify(res));
          } else {
            response.statusCode = 404;
            response.end(`test case '${name}' not found`);
          }
        } catch (err) {
          response.statusCode = 500;
          response.end(`run failed with error: ${err}`);
        }
      } else if (request.url.startsWith(terminatePrefix)) {
        server.close();
        sys.exit(1);
      } else {
        response.statusCode = 404;
        response.end('unhandled url request');
      }
    }
  );

  server.listen(0, () => {
    const address = server.address() as AddressInfo;
    console.log(`Server listening at [[${address.port}]]`);
  });
})().catch(ex => {
  console.error(ex.stack ?? ex.toString());
  sys.exit(1);
});
