type TestFn = () => unknown | Promise<unknown>;

interface TestCase {
  name: string;
  run: TestFn;
  afterEachFns: TestFn[];
}

const tests: TestCase[] = [];
const suiteStack: string[] = [];
const afterEachStack: TestFn[][] = [[]];

function formatValue(value: unknown): string {
  return typeof value === "string" ? `"${value}"` : JSON.stringify(value);
}

function fail(message: string): never {
  throw new Error(message);
}

function assertThrows(
  actual: unknown,
  expectedMessage?: string,
  inverted = false,
): void {
  if (typeof actual !== "function") {
    fail("Expected value to be a function");
  }

  let thrown: unknown;
  try {
    actual();
  } catch (error) {
    thrown = error;
  }

  if (inverted) {
    if (thrown)
      fail(
        `Expected function not to throw, but it threw ${thrown instanceof Error ? thrown.message : String(thrown)}`,
      );
    return;
  }

  if (!thrown) fail("Expected function to throw");
  if (expectedMessage) {
    const message = thrown instanceof Error ? thrown.message : String(thrown);
    if (!message.includes(expectedMessage)) {
      fail(
        `Expected thrown message to include "${expectedMessage}", got "${message}"`,
      );
    }
  }
}

export function describe(name: string, run: () => void): void {
  suiteStack.push(name);
  afterEachStack.push([]);
  try {
    run();
  } finally {
    afterEachStack.pop();
    suiteStack.pop();
  }
}

export function it(name: string, run: TestFn): void {
  tests.push({
    name: [...suiteStack, name].join(" > "),
    run,
    afterEachFns: afterEachStack.flat(),
  });
}

export function afterEach(run: TestFn): void {
  afterEachStack[afterEachStack.length - 1].push(run);
}

export function expect(actual: unknown) {
  return {
    toBe(expected: unknown): void {
      if (!Object.is(actual, expected)) {
        fail(`Expected ${formatValue(actual)} to be ${formatValue(expected)}`);
      }
    },
    toEqual(expected: unknown): void {
      const actualJson = JSON.stringify(actual);
      const expectedJson = JSON.stringify(expected);
      if (actualJson !== expectedJson) {
        fail(`Expected ${actualJson} to equal ${expectedJson}`);
      }
    },
    toThrow(expectedMessage?: string): void {
      assertThrows(actual, expectedMessage);
    },
    not: {
      toThrow(): void {
        assertThrows(actual, undefined, true);
      },
    },
  };
}

export async function runRegisteredTests(): Promise<{
  passed: number;
  failed: number;
  total: number;
}> {
  let passed = 0;
  let failed = 0;

  for (const test of tests) {
    let failure: unknown;
    try {
      await test.run();
    } catch (error) {
      failure = error;
    }

    for (const hook of [...test.afterEachFns].reverse()) {
      try {
        await hook();
      } catch (error) {
        failure = failure || error;
      }
    }

    if (failure) {
      failed += 1;
      console.error(`FAIL ${test.name}`);
      console.error(
        failure instanceof Error
          ? `  ${failure.message}`
          : `  ${String(failure)}`,
      );
    } else {
      passed += 1;
      console.log(`PASS ${test.name}`);
    }
  }

  const total = passed + failed;
  if (total === 0) {
    console.error("No unit tests were registered.");
    return { passed, failed: 1, total };
  }

  console.log(`\n${passed} passed, ${failed} failed`);
  return { passed, failed, total };
}
