import {ParseOptions, TestParser} from '../../test-parser.js'
import {
  TestCaseError,
  TestCaseResult,
  TestExecutionResult,
  TestGroupResult,
  TestRunResult,
  TestSuiteResult
} from '../../test-results.js'
import {getExceptionSource} from '../../utils/node-utils.js'
import {getBasePath, normalizeFilePath} from '../../utils/path-utils.js'
import {MochaJson, MochaJsonTest} from './mocha-json-types.js'

export class MochaJsonParser implements TestParser {
  assumedWorkDir: string | undefined

  constructor(readonly options: ParseOptions) {}

  async parse(path: string, content: string): Promise<TestRunResult> {
    const mocha = this.getMochaJson(path, content)
    const result = this.getTestRunResult(path, mocha)
    result.sort(true)
    return Promise.resolve(result)
  }

  private getMochaJson(path: string, content: string): MochaJson {
    try {
      return JSON.parse(content)
    } catch (e) {
      throw new Error(`Invalid JSON at ${path}\n\n${e}`)
    }
  }

  private getTestRunResult(resultsPath: string, mocha: MochaJson): TestRunResult {
    const suitesMap: {[path: string]: TestSuiteResult} = {}

    const getSuite = (test: MochaJsonTest): TestSuiteResult => {
      const path = this.getRelativePath(test.file)
      return suitesMap[path] ?? (suitesMap[path] = new TestSuiteResult(path, []))
    }

    for (const test of mocha.passes) {
      const suite = getSuite(test)
      this.processTest(suite, test, 'success')
    }

    for (const test of mocha.failures) {
      const suite = getSuite(test)
      this.processTest(suite, test, 'failed')
    }

    for (const test of mocha.pending) {
      const suite = getSuite(test)
      this.processTest(suite, test, 'skipped')
    }

    const suites = Object.values(suitesMap)
    return new TestRunResult(resultsPath, suites, mocha.stats.duration)
  }

  private processTest(suite: TestSuiteResult, test: MochaJsonTest, result: TestExecutionResult): void {
    const groupName =
      test.fullTitle !== test.title
        ? test.fullTitle.substring(0, test.fullTitle.length - test.title.length).trimEnd()
        : null

    let group = suite.groups.find(grp => grp.name === groupName)
    if (group === undefined) {
      group = new TestGroupResult(groupName, [])
      suite.groups.push(group)
    }

    const error = this.getTestCaseError(test)
    const testCase = new TestCaseResult(test.title, result, test.duration ?? 0, error)
    group.tests.push(testCase)
  }

  private getTestCaseError(test: MochaJsonTest): TestCaseError | undefined {
    const details = test.err.stack
    const message = test.err.message
    if (details === undefined) {
      return undefined
    }

    let path
    let line

    const src = getExceptionSource(details, this.options.trackedFiles, file => this.getRelativePath(file))
    if (src) {
      path = src.path
      line = src.line
    }

    return {
      path,
      line,
      message,
      details
    }
  }

  private getRelativePath(path: string): string {
    path = normalizeFilePath(path)
    const workDir = this.getWorkDir(path)
    if (workDir !== undefined && path.startsWith(workDir)) {
      path = path.substring(workDir.length)
    }
    return path
  }

  private getWorkDir(path: string): string | undefined {
    return (
      this.options.workDir ??
      this.assumedWorkDir ??
      (this.assumedWorkDir = getBasePath(path, this.options.trackedFiles))
    )
  }
}
