import fs from 'fs';
import { ICompilerResult, LLParse } from 'llparse';
import { Dot } from 'llparse-dot';
import {
  Fixture, FixtureResult, IFixtureBuildOptions,
} from 'llparse-test-fixture';
import path from 'path';

import llhttp from '../../src/llhttp';

export type Node = Parameters<LLParse['build']>['0'];

export { FixtureResult };

export type TestType = 'request' | 'response' | 'request-finish' | 'response-finish' |
  'request-lenient-all' | 'response-lenient-all' |
  'request-lenient-headers' | 'response-lenient-headers' |
  'request-lenient-chunked-length' | 'request-lenient-transfer-encoding' |
  'request-lenient-keep-alive' | 'response-lenient-keep-alive' |
  'request-lenient-version' | 'response-lenient-version' |
  'request-lenient-data-after-close' | 'response-lenient-data-after-close' |
  'request-lenient-optional-lf-after-cr' | 'response-lenient-optional-lf-after-cr' |
  'request-lenient-optional-cr-before-lf' | 'response-lenient-optional-cr-before-lf' |
  'request-lenient-optional-crlf-after-chunk' | 'response-lenient-optional-crlf-after-chunk' |
  'request-lenient-spaces-after-chunk-size' | 'response-lenient-spaces-after-chunk-size' |
  'request-lenient-header-value-relaxed' | 'response-lenient-header-value-relaxed' |
  'none' | 'url';

export const allowedTypes: TestType[] = [
  'request',
  'response',
  'request-finish',
  'response-finish',
  'request-lenient-all',
  'response-lenient-all',
  'request-lenient-headers',
  'response-lenient-headers',
  'request-lenient-keep-alive',
  'response-lenient-keep-alive',
  'request-lenient-chunked-length',
  'request-lenient-transfer-encoding',
  'request-lenient-version',
  'response-lenient-version',
  'request-lenient-data-after-close',
  'response-lenient-data-after-close',
  'request-lenient-optional-lf-after-cr',
  'response-lenient-optional-lf-after-cr',
  'request-lenient-optional-cr-before-lf',
  'response-lenient-optional-cr-before-lf',
  'request-lenient-optional-crlf-after-chunk',
  'response-lenient-optional-crlf-after-chunk',
  'request-lenient-spaces-after-chunk-size',
  'response-lenient-spaces-after-chunk-size',
  'request-lenient-header-value-relaxed',
  'response-lenient-header-value-relaxed',
];

const BUILD_DIR = path.join(__dirname, '..', 'tmp');
const CHEADERS_FILE = path.join(BUILD_DIR, 'cheaders.h');

const cheaders = new llhttp.CHeaders().build();
try {
  fs.mkdirSync(BUILD_DIR);
} catch {
  // no-op
}
fs.writeFileSync(CHEADERS_FILE, cheaders);

const fixtures = new Fixture({
  buildDir: path.join(__dirname, '..', 'tmp'),
  extra: [
    '-msse4.2',
    '-DLLHTTP__TEST',
    '-DLLPARSE__ERROR_PAUSE=' + llhttp.constants.ERROR.PAUSED,
    '-include', CHEADERS_FILE,
    path.join(__dirname, 'extra.c'),
  ],
  maxParallel: process.env.LLPARSE_DEBUG ? 1 : undefined,
});

const cache = new Map<Node, ICompilerResult>();

export async function build(
  llparse: LLParse, node: Node, outFile: string,
  options: IFixtureBuildOptions = {},
  ty: TestType = 'none'): Promise<FixtureResult> {
  const dot = new Dot();
  fs.writeFileSync(path.join(BUILD_DIR, outFile + '.dot'),
    dot.build(node));

  let artifacts: ICompilerResult;
  if (cache.has(node)) {
    artifacts = cache.get(node)!;
  } else {
    artifacts = llparse.build(node, {
      c: { header: outFile },
      debug: process.env.LLPARSE_DEBUG ? 'llparse__debug' : undefined,
    });
    cache.set(node, artifacts);
  }

  const extra = options.extra === undefined ? [] : options.extra.slice();

  if (allowedTypes.includes(ty)) {
    extra.push(
      `-DLLPARSE__TEST_INIT=llhttp__test_init_${ty.replace(/-/g, '_')}`);
  }

  if (ty === 'request-finish' || ty === 'response-finish') {
    if (ty === 'request-finish') {
      extra.push('-DLLPARSE__TEST_INIT=llhttp__test_init_request');
    } else {
      extra.push('-DLLPARSE__TEST_INIT=llhttp__test_init_response');
    }
    extra.push('-DLLPARSE__TEST_FINISH=llhttp__test_finish');
  }

  return await fixtures.build(artifacts, outFile, Object.assign(options, {
    extra,
  }));
}
