import { ESLint } from 'eslint'
import path from 'path'
import util from 'util'
import { beforeAll, describe, expect, test } from 'vitest'

/**
 * @typedef {object} EslintTestInfo
 * @property {string} name - the title/message for this test
 * @property {string} filePath - the path to the file to lint
 * @property {number} warningCount - the number of warnings to expect
 * @property {number} errorCount - the number of errors to expect
 */

// TSX is omitted because TypeScript insists on fully knowing the React types,
// and I would rather not add React as a dependency of eslint-config-scratch.
/** @type {Record<string, EslintTestInfo[]>} */
const testInfo = {
  recommended: [
    {
      name: 'plain JS (good)',
      filePath: 'plain.good.mjs',
      warningCount: 0,
      errorCount: 0,
    },
    {
      name: 'React JSX (good)',
      filePath: 'react.good.jsx',
      warningCount: 0,
      errorCount: 0,
    },
    {
      name: 'plain JS (bad)',
      filePath: 'plain.bad.mjs',
      warningCount: 0,
      errorCount: 3,
    },
    {
      name: 'React JSX (bad)',
      filePath: 'react.bad.jsx',
      warningCount: 0,
      errorCount: 2,
    },
    {
      name: 'Plain TS (good)',
      filePath: 'plain.good.ts',
      warningCount: 0,
      errorCount: 0,
    },
    {
      name: 'Plain TS (bad)',
      filePath: 'plain.bad.ts',
      warningCount: 0,
      errorCount: 5,
    },
  ],
}

/**
 * Create a snapshot of a lint message.
 * Excludes properties that may change without affecting correctness, such as human-readable text.
 * @param {ESLint.LintMessage} result - the lint message to filter
 * @returns {object} a filtered snapshot of the lint message
 */
const messageSnapshot = result =>
  Object.fromEntries(
    Object.entries(result).filter(([k]) =>
      ['line', 'column', 'endLine', 'endColumn', 'messageId', 'nodeType', 'ruleId'].includes(k),
    ),
  )

test('make sure eslint works at all', async () => {
  const source = 'foo(42)'
  const eslint = new ESLint({
    overrideConfigFile: true,
  })
  const results = await eslint.lintText(source)
  expect(results).toBeDefined()
  expect(results.length).toBe(1)
  expect(results[0].warningCount).toEqual(0)
  expect(results[0].errorCount).toEqual(0)
})

describe.concurrent.for(Object.entries(testInfo))('$0', ([subdir, testList]) => {
  /**
   * @type {ESLint.LintResult[]}
   */
  let results

  // Linting one file at a time takes much longer
  beforeAll(async () => {
    const eslint = new ESLint({
      overrideConfigFile: path.resolve(import.meta.dirname, subdir, 'eslint.config.mjs'),
    })
    results = await eslint.lintFiles(testList.map(info => path.resolve(import.meta.dirname, subdir, info.filePath)))
  })

  test('results container', () => {
    expect(results).toBeDefined()
    expect(results.length).toBe(testList.length)
  })

  testList.forEach(({ name, filePath, warningCount, errorCount }, i) => {
    test(name, () => {
      expect(path.resolve(results[i].filePath)).toBe(path.resolve(import.meta.dirname, subdir, filePath))
      expect(results[i].warningCount, util.inspect(results[i])).toBe(warningCount)
      expect(results[i].errorCount, util.inspect(results[i])).toBe(errorCount)
      expect(results[i].messages.map(messageSnapshot), util.inspect(results[i].messages)).toMatchSnapshot()
    })
  })
})