import readline from "readline"
import TestCase from "./test-case"

// Select properties of the test case that can be used in requirements
export type TestCaseArg = Pick<TestCase, "impl" | "actual" | "result">

interface InteractorOption {
  key: string
  description: string | ((args: TestCaseArg) => string)
  /**
   * The predicate to fulfill in order to display this command option.
   * If this is not defined, then this option is always shown.
   */
  requirement?(args: TestCaseArg): boolean
  /**
   * The function to call to resolve this option.
   * If the option returns a string, print that string and prompt again.
   * Otherwise, quit the loop.
   */
  resolve(args: TestCase): string | Promise<string | void>
}

const options: InteractorOption[] = [
  {
    key: "t",
    description: "Show me the test case.",
    resolve: (test) => test.dir.asArchive(),
  },
  {
    key: "o",
    description: "Show output.",
    requirement(test) {
      if (test.result().failureType === "warning_difference") return false
      return test.actual().isSuccess
    },
    resolve(test) {
      const actual = test.actual()
      if (!actual.isSuccess) {
        throw new Error(`Trying to list output for non-successful result`)
      }
      return actual.output
    },
  },
  {
    key: "e",
    description(test) {
      return test.actual().isSuccess ? "Show warning." : "Show error."
    },
    requirement(test) {
      const actual = test.actual()
      // show this option if the actual result was a failure or it has a warning
      return !actual.isSuccess || !!actual.warning
    },
    resolve(test) {
      const actual = test.actual()
      return actual.isSuccess ? actual.warning ?? "" : actual.error
    },
  },
  {
    key: "d",
    description: "Show diff.",
    requirement: (test) => !!test.result().diff,
    resolve: (test) => test.result().diff!,
  },
  {
    key: "O",
    description: "Update expected output and pass test",
    requirement: (test) => test.result().failureType !== "unnecessary_todo",
    resolve: (test) => test.overwrite(),
  },
  {
    key: "I",
    description: (test) => `Migrate copy of test to pass on ${test.impl}`,
    requirement: (test) => test.result().failureType !== "unnecessary_todo",
    resolve: (test) => test.migrateImpl(),
  },
  {
    key: "T",
    description(test) {
      const word =
        test.result().failureType === "warning_difference" ? "warning" : "spec"
      return `Mark ${word} as todo for ${test.impl}`
    },
    requirement: (test) => test.result().failureType !== "unnecessary_todo",
    resolve: (test) => test.markTodo(),
  },
  {
    key: "G",
    description: (test) => `Ignore test for ${test.impl} FOREVER`,
    requirement: (test) => test.result().failureType !== "unnecessary_todo",
    resolve: (test) => test.markIgnore(),
  },
  {
    key: "f",
    description: "Mark as failed.",
    async resolve() {},
  },
  {
    key: "X",
    description: "Exit testing.",
    async resolve() {
      process.kill(process.pid, "SIGINT")
      // since our cleanup is asynchronous,
      // stall so that we give the cleanup code enough time to finish
      // and kill the process without running more tests
      await new Promise(() => {})
    },
  },
]

export function optionsFor(test: TestCaseArg): InteractorOption[] {
  const result = []
  for (const option of options) {
    if (!option.requirement || option.requirement(test)) {
      result.push(option)
    }
  }
  return result
}

export class Interactor {
  private memory: Record<string, InteractorOption> = {}
  private input: NodeJS.ReadableStream
  private output: NodeJS.WritableStream

  constructor(input: NodeJS.ReadableStream, output: NodeJS.WritableStream) {
    this.input = input
    this.output = output
  }

  private printLine(line: string = ""): void {
    this.output.write(`${line}\n`)
  }

  private printOptions(options: InteractorOption[], test: TestCase): void {
    for (const { key, description } of options) {
      const _description =
        typeof description === "string" ? description : description(test)
      this.output.write(`${key}. ${_description}\n`)
    }
  }

  // Prints content bounded by a delimiter
  private printContent(content: string): void {
    const width = Math.max(...content.split("\n").map((l) => l.length))
    const delimiter = Array(width).fill("*").join("")
    this.printLine()
    this.printLine(delimiter)
    this.printLine(content)
    this.printLine(delimiter)
    this.printLine()
  }

  /**
   * Run the interactor prompt on the given test case.
   */
  async prompt(test: TestCase): Promise<void> {
    const rl = readline.createInterface(this.input, this.output)

    function question(prompt: string): Promise<string> {
      return new Promise((resolve) => {
        rl.question(prompt, resolve)
      })
    }

    const type = test.result().failureType || ""

    // If a repeated choice is chosen for a given failure type, run that choice
    if (this.memory[type]) {
      const choice = this.memory[type]
      await choice.resolve(test)
      rl.close()
      return
    }

    while (true) {
      this.printLine()
      this.printLine(`In test case: ${test.dir.relPath()}`)
      this.printLine(test.result().message)

      const validOptions = optionsFor(test)
      this.printOptions(validOptions, test)

      const answer = await question("Please select an option > ")
      const repeat = answer.endsWith("!")
      const key = answer.replace(/!$/g, "")
      const choice = validOptions.find((o) => o.key === key)
      if (!choice) {
        this.printLine(`Invalid option chosen: ${key}`)
        continue
      }
      const newResult = await choice.resolve(test)
      if (typeof newResult === "string") {
        if (repeat) {
          this.printLine(`Repeat (!) selected on print option. Ignoring...`)
        }
        this.printContent(newResult)
      } else {
        // If the repeat option is chosen, store the chosen choice
        if (repeat) {
          this.memory[type] = choice
        }
        rl.close()
        return
      }
    }
  }
}
