/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-multi-assign */
import * as path from 'node:path';
import * as fs from 'node:fs';
import * as util from 'node:util';
import {
  readList, type Stack, type SupervisorToWorker, type Test, type WorkerToSupervisor,
} from '../base.mts';
import { createRealm, createAgent } from '../base.mts';
import {
  AbruptCompletion, ObjectValue, evalQ,
  setSurroundingAgent,
  inspect,
  Value,
  IsCallable,
  IsDataDescriptor,
  JSStringValue,
  skipDebugger,
  boostTest262Harness,
  ThrowCompletion,
  getHostDefinedErrorStack,
  CallSite,
} from '#self';

const TEST262 = process.env.TEST262 || path.resolve(import.meta.dirname, 'test262');
const TEST262_TESTS = path.join(TEST262, 'test');

const featureMap: Record<string, string> = Object.create(null);
readList(path.resolve(import.meta.dirname, 'features')).forEach((f) => {
  if (f.includes('=')) {
    const [k, v] = f.split('=');
    featureMap[k.trim()] = v.trim();
  }
});

const includeCache: Record<string, undefined | { source: string, specifier: string }> = {};

process.on('message', (test: SupervisorToWorker) => {
  try {
    process.send!({ status: 'RUNNING', testId: test.id } satisfies WorkerToSupervisor, handleSendError);
    const result = run(test);
    if (result.status === 'PASS') {
      process.send!({
        status: 'PASS', file: test.file, flags: test.currentTestFlag, testId: test.id,
      } satisfies WorkerToSupervisor, handleSendError);
    } else {
      process.send!(result satisfies WorkerToSupervisor, handleSendError);
    }
  } catch (e) {
    process.send!(fails(test, util.inspect(e), []), handleSendError);
  }
});

function run(test: Test): WorkerToSupervisor {
  const features = [...test.engineFeatures];
  if (test.attrs.features) {
    test.attrs.features.forEach((f) => {
      if (featureMap[f]) {
        features.push(featureMap[f]);
      }
    });
  }
  const agent = createAgent({ features });
  const parsedScripts = new Map<string, string>();
  setSurroundingAgent(agent);
  agent.hostDefinedOptions.errorStackAttachNativeStack = true;
  agent.hostDefinedOptions.onScriptParsed = (script, id) => {
    parsedScripts.set(id, script.ECMAScriptCode.sourceText);
  };


  function fail(test: Test, error: Value): WorkerToSupervisor {
    const stacks = getHostDefinedErrorStack(error);
    const reportStack: Stack[] = [];
    for (const stack of stacks || []) {
      if (!(stack instanceof CallSite)) {
        continue;
      }
      const scriptId = stack.getScriptId();
      if (!scriptId || stack.columnNumber === null || stack.lineNumber === null) {
        continue;
      }
      const source = parsedScripts.get(scriptId);
      const record = agent.parsedSources.get(scriptId);
      if (record?.HostDefined.specifier?.includes('harness')) {
        continue;
      }
      reportStack.push({
        column: stack.columnNumber,
        line: stack.lineNumber,
        source: source === test.content ? undefined : source,
        specifier: stack.getSpecifier(),
      });
    }
    return {
      status: 'FAIL',
      file: test.file,
      flags: test.currentTestFlag,
      testId: test.id,
      description: test.attrs.description,
      error: inspect(error),
      stack: reportStack,
    };
  }

  const { realm, resolverCache, setPrintHandle } = createRealm({ specifier: test.specifier });
  const r = realm.scope((): WorkerToSupervisor => {
    test.attrs.includes.unshift('assert.js', 'sta.js');
    if (test.attrs.flags.async) {
      test.attrs.includes.unshift('doneprintHandle.js');
    }

    for (const include of test.attrs.includes) {
      if (includeCache[include] === undefined) {
        const p = path.resolve(TEST262, `harness/${include}`);
        includeCache[include] = {
          source: fs.readFileSync(p, 'utf8'),
          specifier: p,
        };
      }
      const entry = includeCache[include];
      const completion = realm.evaluateScript(entry.source, { specifier: entry.specifier });
      if (completion instanceof AbruptCompletion) {
        return fail(test, completion.Value);
      }
    }
    boostTest262Harness(realm);

    {
      const DONE = `
function $DONE(error) {
  if (error) {
    if (typeof error === 'object' && error !== null && 'stack' in error) {
      print('Test262:AsyncTestFailure:' + error.stack, error);
    } else {
      print('Test262:AsyncTestFailure:Test262Error: ' + error, error);
    }
  } else {
    print('Test262:AsyncTestComplete');
  }
}`;
      const completion = realm.evaluateScript(`\
var Test262Error = class Test262Error extends Error {};
Test262Error.thrower = (...args) => {
  throw new Test262Error(...args);
};
${test.attrs.flags.async ? DONE : ''}`);
      if (completion instanceof AbruptCompletion) {
        return fail(test, completion.Value);
      }
    }

    let asyncResult: WorkerToSupervisor | undefined;
    if (test.attrs.flags.async) {
      setPrintHandle((m, value) => {
        if (m === 'Test262:AsyncTestComplete') {
          asyncResult = {
            status: 'PASS', flags: test.currentTestFlag, testId: test.id, file: test.file,
          };
        } else {
          asyncResult = fail(test, value);
        }
        setPrintHandle(undefined);
      });
    }

    const specifier = path.resolve(TEST262_TESTS, test.file);

    const completion = evalQ((Q) => {
      if (test.attrs.flags.module) {
        const module = Q(realm.compileModule(test.content, { specifier }));
        resolverCache.set(specifier, module);
        const loadModuleCompletion = module.LoadRequestedModules();
        if (loadModuleCompletion.PromiseState === 'rejected') {
          Q(ThrowCompletion(loadModuleCompletion.PromiseResult!));
        } else if (loadModuleCompletion.PromiseState === 'pending') {
          throw new Error('Internal error: .LoadRequestedModules() returned a pending promise');
        }
        Q(module.Link());
        const evaluateCompletion = Q(skipDebugger(module.Evaluate()));
        if (evaluateCompletion.PromiseState === 'rejected') {
          Q(ThrowCompletion(evaluateCompletion.PromiseResult!));
        }
      } else {
        Q(realm.evaluateScript(test.content, { specifier }));
      }
    });

    if (completion.Type === 'throw') {
      if (test.attrs.negative && isError(test.attrs.negative.type, completion.Value)) {
        return {
          status: 'PASS', flags: test.currentTestFlag, testId: test.id, file: test.file,
        };
      } else {
        return fail(test, completion.Value);
      }
    }

    if (test.attrs.flags.async) {
      if (!asyncResult) {
        throw new Error('missing async result');
      }
      return asyncResult;
    }

    if (test.attrs.negative) {
      return fails(test, `Expected ${test.attrs.negative.type} during ${test.attrs.negative.phase}`, []);
    } else {
      return {
        status: 'PASS', flags: test.currentTestFlag, testId: test.id, file: test.file,
      };
    }
  });

  return r;
}

function handleSendError(e: any) {
  if (e) {
    console.error(e);
    process.exit(1);
  }
}

function isError(type: string, value: unknown) {
  if (!(value instanceof ObjectValue)) {
    return false;
  }
  const proto = (value as any).Prototype;
  if (!proto || !(proto instanceof ObjectValue)) {
    return false;
  }
  const ctorDesc = proto.properties.get(Value('constructor'));
  if (!ctorDesc || !IsDataDescriptor(ctorDesc)) {
    return false;
  }
  const ctor = ctorDesc.Value;
  if (!(ctor instanceof ObjectValue) || !IsCallable(ctor)) {
    return false;
  }
  const namePropDesc = ctor.properties.get(Value('name'));
  if (!namePropDesc || !IsDataDescriptor(namePropDesc)) {
    return false;
  }
  const nameProp = namePropDesc.Value;
  return nameProp instanceof JSStringValue && nameProp.stringValue() === type;
}

function fails(test: Test, error: string, stack: Stack[]): WorkerToSupervisor {
  return {
    status: 'FAIL',
    file: test.file,
    flags: test.currentTestFlag,
    testId: test.id,
    description: test.attrs.description,
    error,
    stack,
  };
}
