import type { UserConsoleLog } from 'vitest'
import type {
  ReportedHookContext,
  Reporter,
  SerializedError,
  TestCase,
  TestModule,
  TestRunEndReason,
  TestSpecification,
  TestSuite,
  TestUserConfig,
  Vitest,
} from 'vitest/node'
import { rmSync } from 'node:fs'
import { resolve, sep } from 'node:path'
import { describe, expect, onTestFinished, test } from 'vitest'
import { runInlineTests, ts } from '../../test-utils'

describe('TestRun', () => {
  test('pass test run without files (no-watch)', async () => {
    const report = await run(
      {},
      {
        passWithNoTests: true,
        watch: false,
      },
      {
        printTestRunEvents: true,
        failed: true,
      },
    )

    expect(report).toMatchInlineSnapshot(`
      "
      onTestRunStart (0 specifications)
      onTestRunEnd   (passed, 0 modules, 0 errors)"
    `)
  })

  test('pass test run without files (watch)', async () => {
    const report = await run(
      {},
      {
        passWithNoTests: true,
        watch: true,
      },
      {
        printTestRunEvents: true,
        failed: true,
      },
    )

    expect(report).toMatchInlineSnapshot(`
      "
      onTestRunStart (0 specifications)
      onTestRunEnd   (passed, 0 modules, 0 errors)"
    `)
  })

  test('fail test run without files (no-watch)', async () => {
    const report = await run(
      {},
      {
        passWithNoTests: false,
        watch: false,
      },
      {
        printTestRunEvents: true,
        failed: true,
      },
    )

    expect(report).toMatchInlineSnapshot(`
      "
      onTestRunStart (0 specifications)
      onTestRunEnd   (failed, 0 modules, 0 errors)"
    `)
  })
})

describe('TestModule', () => {
  test('single test module', async () => {
    const report = await run({
      'test-module.test.ts': ts`
        test('example', () => {});
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (test-module.test.ts)
      onTestModuleCollected (test-module.test.ts)
      onTestModuleStart     (test-module.test.ts)
        onTestCaseReady     (test-module.test.ts) |example|
        onTestCaseResult    (test-module.test.ts) |example|
      onTestModuleEnd       (test-module.test.ts)"
    `)
  })

  test('multiple test modules', async () => {
    const report = await run({
      'first.test.ts': ts`
        test('first test case', () => {});
      `,
      'second.test.ts': ts`
        test('second test case', () => {});
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (first.test.ts)
      onTestModuleCollected (first.test.ts)
      onTestModuleStart     (first.test.ts)
        onTestCaseReady     (first.test.ts) |first test case|
        onTestCaseResult    (first.test.ts) |first test case|
      onTestModuleEnd       (first.test.ts)

      onTestModuleQueued    (second.test.ts)
      onTestModuleCollected (second.test.ts)
      onTestModuleStart     (second.test.ts)
        onTestCaseReady     (second.test.ts) |second test case|
        onTestCaseResult    (second.test.ts) |second test case|
      onTestModuleEnd       (second.test.ts)"
    `)
  })

  test('test modules with delay', async () => {
    const report = await run({
      'first.test.ts': ts`
        ${delay()}
        test('first test case', async () => { ${delay()} });
      `,
      'second.test.ts': ts`
      ${delay()}
      test('second test case', async () => { ${delay()} });
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (first.test.ts)
      onTestModuleCollected (first.test.ts)
      onTestModuleStart     (first.test.ts)
        onTestCaseReady     (first.test.ts) |first test case|
        onTestCaseResult    (first.test.ts) |first test case|
      onTestModuleEnd       (first.test.ts)

      onTestModuleQueued    (second.test.ts)
      onTestModuleCollected (second.test.ts)
      onTestModuleStart     (second.test.ts)
        onTestCaseReady     (second.test.ts) |second test case|
        onTestCaseResult    (second.test.ts) |second test case|
      onTestModuleEnd       (second.test.ts)"
    `)
  })
})

describe('TestCase', () => {
  test('single test case', async () => {
    const report = await run({
      'example.test.ts': ts`
        test('single test case', async () => {
          await new Promise(resolve => setTimeout(resolve, 300))
          console.log("Test running!")
        });
      `,
    }, {}, { printTestRunEvents: true })

    // console.log is reported outside of the test run cycle
    expect(report).toMatchInlineSnapshot(`
      "
      onTestRunStart (1 specifications)
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestCaseReady     (example.test.ts) |single test case|
        onTestCaseResult    (example.test.ts) |single test case|
      onTestModuleEnd       (example.test.ts)

      onTestRunEnd   (passed, 1 modules, 0 errors)

      onUserConsoleLog (example.test.ts) |single test case| > Test running!"
    `)
  })

  test('multiple test cases', async () => {
    const report = await run({
      'example.test.ts': ts`
        test('first', () => {});
        test('second', () => {});
        test('third', () => {});
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestCaseReady     (example.test.ts) |first|
        onTestCaseResult    (example.test.ts) |first|
        onTestCaseReady     (example.test.ts) |second|
        onTestCaseResult    (example.test.ts) |second|
        onTestCaseReady     (example.test.ts) |third|
        onTestCaseResult    (example.test.ts) |third|
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('multiple test cases with delay', async () => {
    const report = await run({
      'example.test.ts': ts`
        ${delay()}
        test('first', async () => { ${delay()} });
        test('second', async () => { ${delay()} });
        test('third', async () => { ${delay()} });
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestCaseReady     (example.test.ts) |first|
        onTestCaseResult    (example.test.ts) |first|
        onTestCaseReady     (example.test.ts) |second|
        onTestCaseResult    (example.test.ts) |second|
        onTestCaseReady     (example.test.ts) |third|
        onTestCaseResult    (example.test.ts) |third|
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('failing test case', async () => {
    const report = await run({
      'example.test.ts': ts`
        test('failing test case', () => {
          expect(1).toBe(2)
        });
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestCaseReady     (example.test.ts) |failing test case|
        onTestCaseResult    (example.test.ts) |failing test case|
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('skipped test case', async () => {
    const report = await run({
      'example.test.ts': ts`
        test('running', () => {});
        test.skip('skipped', () => {});
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestCaseReady     (example.test.ts) |running|
        onTestCaseResult    (example.test.ts) |running|
        onTestCaseReady     (example.test.ts) |skipped|
        onTestCaseResult    (example.test.ts) |skipped|
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('skipped test case in a different order', async () => {
    const report = await run({
      'example.test.ts': ts`
        test.skip('skipped', () => {});
        test('running', () => {});
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestCaseReady     (example.test.ts) |skipped|
        onTestCaseResult    (example.test.ts) |skipped|
        onTestCaseReady     (example.test.ts) |running|
        onTestCaseResult    (example.test.ts) |running|
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('skipped test case in a suite with a different order', async () => {
    const report = await run({
      'example.test.ts': ts`
        describe('suite', () => {
          test.skip('skipped', () => {});
          test('running', () => {});
        })
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestSuiteReady    (example.test.ts) |suite|
          onTestCaseReady   (example.test.ts) |skipped|
          onTestCaseResult  (example.test.ts) |skipped|
          onTestCaseReady   (example.test.ts) |running|
          onTestCaseResult  (example.test.ts) |running|
        onTestSuiteResult   (example.test.ts) |suite|
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('dynamically skipped test case', async () => {
    const report = await run({
      'example.test.ts': ts`
        test('running', () => {});
        test('skipped', (ctx) => { ctx.skip() });
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestCaseReady     (example.test.ts) |running|
        onTestCaseResult    (example.test.ts) |running|
        onTestCaseReady     (example.test.ts) |skipped|
        onTestCaseResult    (example.test.ts) |skipped|
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('skipped all test cases', async () => {
    const report = await run({
      'example.test.ts': ts`
        test.skip('first', () => {});
        test.skip('second', () => {});
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestCaseReady     (example.test.ts) |first|
        onTestCaseResult    (example.test.ts) |first|
        onTestCaseReady     (example.test.ts) |second|
        onTestCaseResult    (example.test.ts) |second|
      onTestModuleEnd       (example.test.ts)"
    `)
  })
})

describe('TestSuite', () => {
  test('single test suite', async () => {
    const report = await run({
      'example.test.ts': ts`
        describe("example suite", () => {
          test('first test case', () => {});
        });
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestSuiteReady    (example.test.ts) |example suite|
          onTestCaseReady   (example.test.ts) |first test case|
          onTestCaseResult  (example.test.ts) |first test case|
        onTestSuiteResult   (example.test.ts) |example suite|
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('multiple test suites', async () => {
    const report = await run({
      'example.test.ts': ts`
        describe("first suite", () => {
          test('first test case', () => {});
        });

        describe("second suite", () => {
          test('second test case', () => {});
        });
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestSuiteReady    (example.test.ts) |first suite|
          onTestCaseReady   (example.test.ts) |first test case|
          onTestCaseResult  (example.test.ts) |first test case|
        onTestSuiteResult   (example.test.ts) |first suite|
        onTestSuiteReady    (example.test.ts) |second suite|
          onTestCaseReady   (example.test.ts) |second test case|
          onTestCaseResult  (example.test.ts) |second test case|
        onTestSuiteResult   (example.test.ts) |second suite|
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('multiple test suites with delay', async () => {
    const report = await run({
      'example.test.ts': ts`
        ${delay()}
        describe("first suite", async () => {
          ${delay()}
          test('first test case', async () => { ${delay()} });
        });

        describe("second suite", async () => {
          ${delay()}
          test('second test case', async () => { ${delay()} });
        });
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestSuiteReady    (example.test.ts) |first suite|
          onTestCaseReady   (example.test.ts) |first test case|
          onTestCaseResult  (example.test.ts) |first test case|
        onTestSuiteResult   (example.test.ts) |first suite|
        onTestSuiteReady    (example.test.ts) |second suite|
          onTestCaseReady   (example.test.ts) |second test case|
          onTestCaseResult  (example.test.ts) |second test case|
        onTestSuiteResult   (example.test.ts) |second suite|
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('nested test suites', async () => {
    const report = await run({
      'example.test.ts': ts`
        describe("first suite", () => {
          test('first test case', () => {});

          describe("second suite", () => {
            test('second test case', () => {});

            describe("third suite", () => {
              test('third test case', () => {});
            });
          });
        });
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestSuiteReady    (example.test.ts) |first suite|
          onTestCaseReady   (example.test.ts) |first test case|
          onTestCaseResult  (example.test.ts) |first test case|
          onTestSuiteReady  (example.test.ts) |second suite|
            onTestCaseReady (example.test.ts) |second test case|
            onTestCaseResult (example.test.ts) |second test case|
            onTestSuiteReady (example.test.ts) |third suite|
              onTestCaseReady (example.test.ts) |third test case|
              onTestCaseResult (example.test.ts) |third test case|
            onTestSuiteResult (example.test.ts) |third suite|
          onTestSuiteResult (example.test.ts) |second suite|
        onTestSuiteResult   (example.test.ts) |first suite|
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('skipped test suite', async () => {
    const report = await run({
      'example.test.ts': ts`
        describe.skip("skipped suite", () => {
          test('first test case', () => {});
        });
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestSuiteReady    (example.test.ts) |skipped suite|
          onTestCaseReady   (example.test.ts) |first test case|
          onTestCaseResult  (example.test.ts) |first test case|
        onTestSuiteResult   (example.test.ts) |skipped suite|
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('skipped double nested test suite', async () => {
    const report = await run({
      'example.test.ts': ts`
        describe.skip("skipped suite", () => {
          describe.skip("nested skipped suite", () => {
            test('first nested case', () => {});
          })
        });

        test('first test case', () => {});
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestSuiteReady    (example.test.ts) |skipped suite|
          onTestSuiteReady  (example.test.ts) |nested skipped suite|
            onTestCaseReady (example.test.ts) |first nested case|
            onTestCaseResult (example.test.ts) |first nested case|
          onTestSuiteResult (example.test.ts) |nested skipped suite|
        onTestSuiteResult   (example.test.ts) |skipped suite|
        onTestCaseReady     (example.test.ts) |first test case|
        onTestCaseResult    (example.test.ts) |first test case|
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('skipped nested test suite', async () => {
    const report = await run({
      'example.test.ts': ts`
        describe("first suite", () => {
          test('first test case', () => {});

          describe.skip("skipped suite", () => {
            test('second test case', () => {});
          });
        });
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestSuiteReady    (example.test.ts) |first suite|
          onTestCaseReady   (example.test.ts) |first test case|
          onTestCaseResult  (example.test.ts) |first test case|
          onTestSuiteReady  (example.test.ts) |skipped suite|
            onTestCaseReady (example.test.ts) |second test case|
            onTestCaseResult (example.test.ts) |second test case|
          onTestSuiteResult (example.test.ts) |skipped suite|
        onTestSuiteResult   (example.test.ts) |first suite|
      onTestModuleEnd       (example.test.ts)"
    `)
  })
})

describe('hooks', () => {
  test('beforeEach', async () => {
    const report = await run({
      'example.test.ts': ts`
        beforeEach(() => {});

        test('first', () => {});
        test('second', () => {});
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestCaseReady     (example.test.ts) |first|
          onHookStart       (example.test.ts) |first| [beforeEach]
          onHookEnd         (example.test.ts) |first| [beforeEach]
        onTestCaseResult    (example.test.ts) |first|
        onTestCaseReady     (example.test.ts) |second|
          onHookStart       (example.test.ts) |second| [beforeEach]
          onHookEnd         (example.test.ts) |second| [beforeEach]
        onTestCaseResult    (example.test.ts) |second|
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('afterEach', async () => {
    const report = await run({
      'example.test.ts': ts`
        afterEach(() => {});

        test('first', () => {});
        test('second', () => {});
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestCaseReady     (example.test.ts) |first|
          onHookStart       (example.test.ts) |first| [afterEach]
          onHookEnd         (example.test.ts) |first| [afterEach]
        onTestCaseResult    (example.test.ts) |first|
        onTestCaseReady     (example.test.ts) |second|
          onHookStart       (example.test.ts) |second| [afterEach]
          onHookEnd         (example.test.ts) |second| [afterEach]
        onTestCaseResult    (example.test.ts) |second|
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('beforeEach and afterEach', async () => {
    const report = await run({
      'example.test.ts': ts`
        beforeEach(() => {});
        afterEach(() => {});

        test('first', () => {});
        test('second', () => {});
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestCaseReady     (example.test.ts) |first|
          onHookStart       (example.test.ts) |first| [beforeEach]
          onHookEnd         (example.test.ts) |first| [beforeEach]
          onHookStart       (example.test.ts) |first| [afterEach]
          onHookEnd         (example.test.ts) |first| [afterEach]
        onTestCaseResult    (example.test.ts) |first|
        onTestCaseReady     (example.test.ts) |second|
          onHookStart       (example.test.ts) |second| [beforeEach]
          onHookEnd         (example.test.ts) |second| [beforeEach]
          onHookStart       (example.test.ts) |second| [afterEach]
          onHookEnd         (example.test.ts) |second| [afterEach]
        onTestCaseResult    (example.test.ts) |second|
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('beforeAll', async () => {
    const report = await run({
      'example.test.ts': ts`
        beforeAll(() => {});

        test('first', () => {});
        test('second', () => {});
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onHookStart         (example.test.ts) [beforeAll]
        onHookEnd           (example.test.ts) [beforeAll]
        onTestCaseReady     (example.test.ts) |first|
        onTestCaseResult    (example.test.ts) |first|
        onTestCaseReady     (example.test.ts) |second|
        onTestCaseResult    (example.test.ts) |second|
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('afterAll', async () => {
    const report = await run({
      'example.test.ts': ts`
        afterAll(() => {});

        test('first', () => {});
        test('second', () => {});
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestCaseReady     (example.test.ts) |first|
        onTestCaseResult    (example.test.ts) |first|
        onTestCaseReady     (example.test.ts) |second|
        onTestCaseResult    (example.test.ts) |second|
        onHookStart         (example.test.ts) [afterAll]
        onHookEnd           (example.test.ts) [afterAll]
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('beforeAll and afterAll', async () => {
    const report = await run({
      'example.test.ts': ts`
        beforeAll(() => {});
        afterAll(() => {});

        test('first', () => {});
        test('second', () => {});
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onHookStart         (example.test.ts) [beforeAll]
        onHookEnd           (example.test.ts) [beforeAll]
        onTestCaseReady     (example.test.ts) |first|
        onTestCaseResult    (example.test.ts) |first|
        onTestCaseReady     (example.test.ts) |second|
        onTestCaseResult    (example.test.ts) |second|
        onHookStart         (example.test.ts) [afterAll]
        onHookEnd           (example.test.ts) [afterAll]
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('all hooks with delay', async () => {
    const report = await run({
      'example.test.ts': ts`
        ${delay()}
        beforeAll(async () => { ${delay()} });
        afterAll(async () => { ${delay()} });
        beforeEach(async () => { ${delay()} });
        afterEach(async () => { ${delay()} });

        test('first', async () => { ${delay()} });
        test('second', async () => { ${delay()} });
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onHookStart         (example.test.ts) [beforeAll]
        onHookEnd           (example.test.ts) [beforeAll]
        onTestCaseReady     (example.test.ts) |first|
          onHookStart       (example.test.ts) |first| [beforeEach]
          onHookEnd         (example.test.ts) |first| [beforeEach]
          onHookStart       (example.test.ts) |first| [afterEach]
          onHookEnd         (example.test.ts) |first| [afterEach]
        onTestCaseResult    (example.test.ts) |first|
        onTestCaseReady     (example.test.ts) |second|
          onHookStart       (example.test.ts) |second| [beforeEach]
          onHookEnd         (example.test.ts) |second| [beforeEach]
          onHookStart       (example.test.ts) |second| [afterEach]
          onHookEnd         (example.test.ts) |second| [afterEach]
        onTestCaseResult    (example.test.ts) |second|
        onHookStart         (example.test.ts) [afterAll]
        onHookEnd           (example.test.ts) [afterAll]
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('beforeAll on suite', async () => {
    const report = await run({
      'example.test.ts': ts`
        describe("example", () => {
          beforeAll(() => {});

          test('first', () => {});
          test('second', () => {});
        })
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestSuiteReady    (example.test.ts) |example|
          onHookStart       (example.test.ts) |example| [beforeAll]
          onHookEnd         (example.test.ts) |example| [beforeAll]
          onTestCaseReady   (example.test.ts) |first|
          onTestCaseResult  (example.test.ts) |first|
          onTestCaseReady   (example.test.ts) |second|
          onTestCaseResult  (example.test.ts) |second|
        onTestSuiteResult   (example.test.ts) |example|
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('afterAll on suite', async () => {
    const report = await run({
      'example.test.ts': ts`
        describe("example", () => {
          afterAll(() => {});

          test('first', () => {});
          test('second', () => {});
        })
      `,
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestSuiteReady    (example.test.ts) |example|
          onTestCaseReady   (example.test.ts) |first|
          onTestCaseResult  (example.test.ts) |first|
          onTestCaseReady   (example.test.ts) |second|
          onTestCaseResult  (example.test.ts) |second|
          onHookStart       (example.test.ts) |example| [afterAll]
          onHookEnd         (example.test.ts) |example| [afterAll]
        onTestSuiteResult   (example.test.ts) |example|
      onTestModuleEnd       (example.test.ts)"
    `)
  })
})

describe('merge reports', () => {
  test('correctly reports events for a single test module', async () => {
    const blobsOutputDirectory = resolve(import.meta.dirname, 'fixtures-blobs')
    const blobOutputFile = resolve(blobsOutputDirectory, 'blob.json')
    onTestFinished(() => {
      rmSync(blobOutputFile)
    })

    const { root } = await runInlineTests({
      'example.test.ts': ts`
        test('first', () => {});
        describe('suite', () => {
          test('second', () => {});
        });
      `,
    }, {
      globals: true,
      reporters: [['blob', { outputFile: blobOutputFile }]],
    })

    const report = await run(
      {},
      {
        mergeReports: blobsOutputDirectory,
      },
      {
        roots: [root],
      },
    )

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example.test.ts)
      onTestModuleCollected (example.test.ts)
      onTestModuleStart     (example.test.ts)
        onTestCaseReady     (example.test.ts) |first|
        onTestCaseResult    (example.test.ts) |first|
        onTestSuiteReady    (example.test.ts) |suite|
          onTestCaseReady   (example.test.ts) |second|
          onTestCaseResult  (example.test.ts) |second|
        onTestSuiteResult   (example.test.ts) |suite|
      onTestModuleEnd       (example.test.ts)"
    `)
  })

  test('correctly reports multiple test modules', async () => {
    const blobsOutputDirectory = resolve(import.meta.dirname, 'fixtures-blobs')
    const blobOutputFile1 = resolve(blobsOutputDirectory, 'blob-1.json')
    const blobOutputFile2 = resolve(blobsOutputDirectory, 'blob-2.json')
    onTestFinished(() => {
      rmSync(blobOutputFile1)
      rmSync(blobOutputFile2)
    })

    const { root: root1 } = await runInlineTests({
      'example-1.test.ts': ts`
        test('first', () => {});
        describe('suite', () => {
          test('second', () => {});
        });
      `,
    }, {
      globals: true,
      reporters: [['blob', { outputFile: blobOutputFile1 }]],
      watch: false,
    })

    const { root: root2 } = await runInlineTests({
      'example-2.test.ts': ts`
        test('first', () => {});
        describe.skip('suite', () => {
          test('second', () => {});
          test('third', () => {});
        });
        test.skip('fourth', () => {});
        test('fifth', () => {});
        `,
    }, {
      globals: true,
      reporters: [['blob', { outputFile: blobOutputFile2 }]],
      watch: false,
    })

    const report = await run({}, {
      mergeReports: blobsOutputDirectory,
      watch: false,
    }, {
      roots: [root1, root2],
    })

    expect(report).toMatchInlineSnapshot(`
      "
      onTestModuleQueued    (example-1.test.ts)
      onTestModuleCollected (example-1.test.ts)
      onTestModuleStart     (example-1.test.ts)
        onTestCaseReady     (example-1.test.ts) |first|
        onTestCaseResult    (example-1.test.ts) |first|
        onTestSuiteReady    (example-1.test.ts) |suite|
          onTestCaseReady   (example-1.test.ts) |second|
          onTestCaseResult  (example-1.test.ts) |second|
        onTestSuiteResult   (example-1.test.ts) |suite|
      onTestModuleEnd       (example-1.test.ts)

      onTestModuleQueued    (example-2.test.ts)
      onTestModuleCollected (example-2.test.ts)
      onTestModuleStart     (example-2.test.ts)
        onTestCaseReady     (example-2.test.ts) |first|
        onTestCaseResult    (example-2.test.ts) |first|
        onTestSuiteReady    (example-2.test.ts) |suite|
          onTestCaseReady   (example-2.test.ts) |second|
          onTestCaseResult  (example-2.test.ts) |second|
          onTestCaseReady   (example-2.test.ts) |third|
          onTestCaseResult  (example-2.test.ts) |third|
        onTestSuiteResult   (example-2.test.ts) |suite|
        onTestCaseReady     (example-2.test.ts) |fourth|
        onTestCaseResult    (example-2.test.ts) |fourth|
        onTestCaseReady     (example-2.test.ts) |fifth|
        onTestCaseResult    (example-2.test.ts) |fifth|
      onTestModuleEnd       (example-2.test.ts)"
    `)
  })
})

describe('type checking', () => {
  test('typechecking is reported correctly', async () => {
    const report = await run({
      'example-1.test-d.ts': ts`
        test('first', () => {});
        describe('suite', () => {
          test('second', () => {});
        });
      `,
      'example-2.test-d.ts': ts`
        test('first', () => {});
        describe.skip('suite', () => {
          test('second', () => {});
          test('third', () => {});
        });
        test.skip('fourth', () => {});
        test('fifth', () => {});
      `,
      'tsconfig.json': JSON.stringify({
        compilerOptions: {
          strict: true,
        },
        include: ['./*.test-d.ts'],
      }),
    }, {
      typecheck: {
        enabled: true,
      },
    }, { printTestRunEvents: true })

    // NOTE: typechecker reports test modules in bulk, so the order of queued and collect
    // is different from the normal test run, this is because the typechecker runs everything together
    // this _might_ need to be changed in the future
    expect(report).toMatchInlineSnapshot(`
      "
      onTestRunStart (2 specifications)
      onTestModuleQueued    (example-1.test-d.ts)
      onTestModuleQueued    (example-2.test-d.ts)
      onTestModuleCollected (example-1.test-d.ts)
      onTestModuleCollected (example-2.test-d.ts)
      onTestModuleStart     (example-1.test-d.ts)
        onTestCaseReady     (example-1.test-d.ts) |first|
        onTestCaseResult    (example-1.test-d.ts) |first|
        onTestSuiteReady    (example-1.test-d.ts) |suite|
          onTestCaseReady   (example-1.test-d.ts) |second|
          onTestCaseResult  (example-1.test-d.ts) |second|
        onTestSuiteResult   (example-1.test-d.ts) |suite|
      onTestModuleEnd       (example-1.test-d.ts)

      onTestModuleStart     (example-2.test-d.ts)
        onTestCaseReady     (example-2.test-d.ts) |first|
        onTestCaseResult    (example-2.test-d.ts) |first|
        onTestSuiteReady    (example-2.test-d.ts) |suite|
          onTestCaseReady   (example-2.test-d.ts) |second|
          onTestCaseResult  (example-2.test-d.ts) |second|
          onTestCaseReady   (example-2.test-d.ts) |third|
          onTestCaseResult  (example-2.test-d.ts) |third|
        onTestSuiteResult   (example-2.test-d.ts) |suite|
        onTestCaseReady     (example-2.test-d.ts) |fourth|
        onTestCaseResult    (example-2.test-d.ts) |fourth|
        onTestCaseReady     (example-2.test-d.ts) |fifth|
        onTestCaseResult    (example-2.test-d.ts) |fifth|
      onTestModuleEnd       (example-2.test-d.ts)

      onTestRunEnd   (failed, 2 modules, 0 errors)"
    `)
  })
})

describe('test run result', () => {
  test('test run is interrupted', async () => {
    let vitest: Vitest
    let reason: TestRunEndReason | undefined

    await runInlineTests({
      'example.test.js': `
        test('basic', () => new Promise(() => {}))
      `,
    }, {
      globals: true,
      reporters: [
        {
          onInit(ctx) {
            vitest = ctx
          },
          onTestModuleCollected() {
            vitest.cancelCurrentRun('keyboard-input')
          },
          onTestRunEnd(_, __, reason_) {
            reason = reason_
          },
        },
      ],
    })

    expect(reason).toBe('interrupted')
  })

  test('test run failed, but passed afterwards', async () => {
    let reason: TestRunEndReason | undefined

    const { fs } = await runInlineTests({
      'example.test.js': `
        test('basic', () => {
          expect(1).toBe(2)
        })
      `,
    }, {
      globals: true,
      watch: true,
      reporters: [
        {
          onTestRunEnd(_, __, reason_) {
            reason = reason_
          },
        },
      ],
    })

    expect(reason).toBe('failed')

    fs.editFile('./example.test.js', c => c.replace('toBe(2)', 'toBe(1)'))

    await expect.poll(() => reason).toBe('passed')
  })

  test('test run passed', async () => {
    let reason: TestRunEndReason | undefined

    await runInlineTests({
      'example.test.js': `
        test('basic', () => {
          expect(1).toBe(1)
        })
      `,
    }, {
      globals: true,
      watch: true,
      reporters: [
        {
          onTestRunEnd(_, __, reason_) {
            reason = reason_
          },
        },
      ],
    })

    expect(reason).toBe('passed')
  })
})

interface ReporterOptions {
  printTestRunEvents?: boolean
  roots?: string[]
  failed?: boolean
}

async function run(
  structure: Parameters<typeof runInlineTests>[0],
  customConfig?: TestUserConfig,
  reporterOptions?: ReporterOptions,
) {
  const reporter = new CustomReporter(reporterOptions)

  const config: TestUserConfig = {
    config: false,
    fileParallelism: false,
    globals: true,
    reporters: [reporter],
    sequence: {
      sequencer: class Sorter {
        sort(files: TestSpecification[]) {
          return files.sort((a, b) => a.moduleId.localeCompare(b.moduleId))
        }

        shard(files: TestSpecification[]) {
          return files
        }
      },
    },
    ...customConfig,
  }

  const { stdout, stderr } = await runInlineTests(structure, config)

  if (!reporterOptions?.printTestRunEvents && !reporterOptions?.failed) {
    expect(stdout).toBe('')
    expect(stderr).toBe('')
  }

  return `\n${reporter.calls.join('\n').trim()}`
}

class CustomReporter implements Reporter {
  calls: string[] = []
  logs: string[] = []
  ctx!: Vitest

  constructor(private options: ReporterOptions = {}) {}

  onInit(ctx: Vitest) {
    this.ctx = ctx
  }

  onTestRunStart(specifications: ReadonlyArray<TestSpecification>) {
    if (this.options.printTestRunEvents) {
      this.calls.push(`onTestRunStart (${specifications.length} specifications)`)
    }
  }

  onTestRunEnd(modules: ReadonlyArray<TestModule>, errors: ReadonlyArray<SerializedError>, state: TestRunEndReason) {
    if (this.options.printTestRunEvents) {
      this.calls.push(`onTestRunEnd   (${state}, ${modules.length} modules, ${errors.length} errors)`)
      if (this.logs.length) {
        this.calls.push('', ...this.logs)
      }
    }
  }

  onTestModuleQueued(module: TestModule) {
    this.calls.push(`onTestModuleQueued    (${this.normalizeFilename(module)})`)
  }

  onTestModuleCollected(module: TestModule) {
    this.calls.push(`onTestModuleCollected (${this.normalizeFilename(module)})`)
  }

  onTestSuiteReady(testSuite: TestSuite) {
    this.calls.push(`${padded(testSuite, 'onTestSuiteReady')} (${this.normalizeFilename(testSuite.module)}) |${testSuite.name}|`)
  }

  onTestSuiteResult(testSuite: TestSuite) {
    this.calls.push(`${padded(testSuite, 'onTestSuiteResult')} (${this.normalizeFilename(testSuite.module)}) |${testSuite.name}|`)
  }

  onTestModuleStart(module: TestModule) {
    this.calls.push(`onTestModuleStart     (${this.normalizeFilename(module)})`)
  }

  onTestModuleEnd(module: TestModule) {
    this.calls.push(`onTestModuleEnd       (${this.normalizeFilename(module)})\n`)
  }

  onTestCaseReady(test: TestCase) {
    this.calls.push(`${padded(test, 'onTestCaseReady')} (${this.normalizeFilename(test.module)}) |${test.name}|`)
  }

  onTestCaseResult(test: TestCase) {
    this.calls.push(`${padded(test, 'onTestCaseResult')} (${this.normalizeFilename(test.module)}) |${test.name}|`)
  }

  onUserConsoleLog(log: UserConsoleLog) {
    const task = this.ctx.state.idMap.get(log.taskId!)
    const test = task && this.ctx.state.getReportedEntity(task) as TestCase

    this.logs.push(`onUserConsoleLog (${this.normalizeFilename(test!.module)}) |${test!.name}| > ${log.content.replaceAll('\n', '')}`)
  }

  onHookStart(hook: ReportedHookContext) {
    const module = hook.entity.type === 'module' ? hook.entity : hook.entity.module
    const name = hook.entity.type !== 'module' ? ` |${hook.entity.name}|` : ''
    this.calls.push(`  ${padded(hook.entity, 'onHookStart', 19)} (${this.normalizeFilename(module)})${name} [${hook.name}]`)
  }

  onHookEnd(hook: ReportedHookContext) {
    const module = hook.entity.type === 'module' ? hook.entity : hook.entity.module
    const name = hook.entity.type !== 'module' ? ` |${hook.entity.name}|` : ''
    this.calls.push(`  ${padded(hook.entity, 'onHookEnd', 19)} (${this.normalizeFilename(module)})${name} [${hook.name}]`)
  }

  normalizeFilename(module: TestModule) {
    return normalizeFilename(module, this.options.roots)
  }
}

function normalizeFilename(module: TestModule, roots?: string[]) {
  const relative = (roots || [module.project.config.root]).reduce((acc, root) => {
    return acc.replace(root, '')
  }, module.moduleId)
  return relative.replaceAll(sep, '/')
    .substring(1)
}

function padded(entity: TestSuite | TestCase | TestModule, name: string, pad = 21) {
  return (' '.repeat(getDepth(entity)) + name).padEnd(pad)
}

function getDepth(entity: TestSuite | TestCase | TestModule) {
  if (entity.type === 'module') {
    return 0
  }

  let depth = 0
  let parent = entity.parent

  while (parent) {
    depth += 2
    if (parent.type !== 'module') {
      parent = parent.parent
    }
    else {
      break
    }
  }

  return depth
}

function delay() {
  return `await new Promise(resolve => setTimeout(resolve, 100));`
}
