import { assert, describe, it } from "@effect/vitest"
import { Config, ConfigProvider, Effect, FileSystem, Layer, Option, Path, Ref, Stdio } from "effect"
import { TestConsole } from "effect/testing"
import { Argument, CliError, Flag, Prompt } from "effect/unstable/cli"
import { ChildProcessSpawner } from "effect/unstable/process"
import * as MockTerminal from "./services/MockTerminal.ts"

const ConsoleLayer = TestConsole.layer
const FileSystemLayer = FileSystem.layerNoop({})
const PathLayer = Path.layer
const TerminalLayer = MockTerminal.layer
const StdioLayer = Stdio.layerTest({})
const ChildProcessSpawnerLayer = Layer.succeed(
  ChildProcessSpawner.ChildProcessSpawner,
  ChildProcessSpawner.make(() => Effect.die("Not implemented"))
)

const TestLayer = Layer.mergeAll(
  ConsoleLayer,
  FileSystemLayer,
  PathLayer,
  TerminalLayer,
  StdioLayer,
  ChildProcessSpawnerLayer
)

describe("Param", () => {
  describe("optional", () => {
    it.effect("returns none when an optional boolean flag is omitted", () =>
      Effect.gen(function*() {
        const flag = Flag.boolean("verbose").pipe(Flag.optional)

        const [, value] = yield* flag.parse({
          flags: {},
          arguments: []
        })

        assert.deepStrictEqual(value, Option.none())
      }).pipe(Effect.provide(TestLayer)))

    it.effect("returns some when an optional boolean flag is provided", () =>
      Effect.gen(function*() {
        const flag = Flag.boolean("verbose").pipe(Flag.optional)

        const [, value] = yield* flag.parse({
          flags: { verbose: ["false"] },
          arguments: []
        })

        assert.deepStrictEqual(value, Option.some(false))
      }).pipe(Effect.provide(TestLayer)))
  })

  describe("withFallbackPrompt", () => {
    it.effect("prompts for missing flag values and preserves remaining args", () =>
      Effect.gen(function*() {
        const prompt = Prompt.text({ message: "Name" })
        const flag = Flag.string("name").pipe(Flag.withFallbackPrompt(prompt))

        yield* MockTerminal.inputText("Chandra")
        yield* MockTerminal.inputKey("enter")

        const [remaining, value] = yield* flag.parse({
          flags: {},
          arguments: ["tail"]
        })

        assert.strictEqual(value, "Chandra")
        assert.deepStrictEqual(remaining, ["tail"])
      }).pipe(Effect.provide(TestLayer)))

    it.effect("does not prompt when flag value is provided", () =>
      Effect.gen(function*() {
        const prompt = Prompt.text({ message: "Name" })
        const flag = Flag.string("name").pipe(Flag.withFallbackPrompt(prompt))

        const [, value] = yield* flag.parse({
          flags: { name: ["Ava"] },
          arguments: []
        })

        assert.strictEqual(value, "Ava")
      }).pipe(Effect.provide(TestLayer)))

    it.effect("prompts for missing arguments", () =>
      Effect.gen(function*() {
        const prompt = Prompt.text({ message: "File" })
        const argument = Argument.string("file").pipe(Argument.withFallbackPrompt(prompt))

        yield* MockTerminal.inputText("notes.txt")
        yield* MockTerminal.inputKey("enter")

        const [remaining, value] = yield* argument.parse({
          flags: {},
          arguments: []
        })

        assert.strictEqual(value, "notes.txt")
        assert.deepStrictEqual(remaining, [])
      }).pipe(Effect.provide(TestLayer)))

    it.effect("builds flag fallback prompts from effects lazily", () =>
      Effect.gen(function*() {
        const calls = yield* Ref.make(0)
        const prompt = Effect.gen(function*() {
          yield* Ref.update(calls, (n) => n + 1)
          return Prompt.text({ message: "Name from effect" })
        })

        const flag = Flag.string("name").pipe(Flag.withFallbackPrompt(prompt))

        const [, provided] = yield* flag.parse({
          flags: { name: ["Ava"] },
          arguments: []
        })
        assert.strictEqual(provided, "Ava")
        assert.strictEqual(yield* Ref.get(calls), 0)

        yield* MockTerminal.inputText("Mia")
        yield* MockTerminal.inputKey("enter")

        const [remaining, prompted] = yield* flag.parse({
          flags: {},
          arguments: ["tail"]
        })

        assert.strictEqual(prompted, "Mia")
        assert.deepStrictEqual(remaining, ["tail"])
        assert.strictEqual(yield* Ref.get(calls), 1)
      }).pipe(Effect.provide(TestLayer)))

    it.effect("builds argument fallback prompts from effects lazily", () =>
      Effect.gen(function*() {
        const calls = yield* Ref.make(0)
        const prompt = Effect.gen(function*() {
          yield* Ref.update(calls, (n) => n + 1)
          return Prompt.text({ message: "File from effect" })
        })

        const argument = Argument.string("file").pipe(Argument.withFallbackPrompt(prompt))

        const [providedRemaining, provided] = yield* argument.parse({
          flags: {},
          arguments: ["notes.txt", "tail"]
        })
        assert.strictEqual(provided, "notes.txt")
        assert.deepStrictEqual(providedRemaining, ["tail"])
        assert.strictEqual(yield* Ref.get(calls), 0)

        yield* MockTerminal.inputText("draft.md")
        yield* MockTerminal.inputKey("enter")

        const [promptedRemaining, prompted] = yield* argument.parse({
          flags: {},
          arguments: []
        })

        assert.strictEqual(prompted, "draft.md")
        assert.deepStrictEqual(promptedRemaining, [])
        assert.strictEqual(yield* Ref.get(calls), 1)
      }).pipe(Effect.provide(TestLayer)))

    it.effect("propagates errors from effectful fallback prompt creation", () =>
      Effect.gen(function*() {
        const failure = new CliError.UserError({
          cause: "failed to build prompt"
        })
        const prompt = Effect.fail(failure)

        const flag = Flag.string("name").pipe(Flag.withFallbackPrompt(prompt))

        const error = yield* Effect.flip(
          flag.parse({
            flags: {},
            arguments: []
          })
        )

        assert.strictEqual(error, failure)
      }).pipe(Effect.provide(TestLayer)))

    it.effect("prefers defaults over fallback prompts", () =>
      Effect.gen(function*() {
        const prompt = Prompt.text({ message: "Name" })
        const flag = Flag.string("name").pipe(
          Flag.withDefault("guest"),
          Flag.withFallbackPrompt(prompt)
        )

        const [, value] = yield* flag.parse({
          flags: {},
          arguments: []
        })

        assert.strictEqual(value, "guest")
      }).pipe(Effect.provide(TestLayer)))

    it.effect("does not prompt for invalid flag values", () =>
      Effect.gen(function*() {
        const prompt = Prompt.text({ message: "Count" })
        const flag = Flag.integer("count").pipe(Flag.withFallbackPrompt(prompt))

        const error = yield* Effect.flip(
          flag.parse({
            flags: { count: ["nope"] },
            arguments: []
          })
        )

        assert.instanceOf(error, CliError.InvalidValue)
      }).pipe(Effect.provide(TestLayer)))

    it.effect("does not prompt for invalid argument values", () =>
      Effect.gen(function*() {
        const prompt = Prompt.text({ message: "Count" })
        const argument = Argument.integer("count").pipe(Argument.withFallbackPrompt(prompt))

        const error = yield* Effect.flip(
          argument.parse({
            flags: {},
            arguments: ["nope"]
          })
        )

        assert.instanceOf(error, CliError.InvalidValue)
      }).pipe(Effect.provide(TestLayer)))

    it.effect("does not prompt for missing boolean flags", () =>
      Effect.gen(function*() {
        const prompt = Prompt.text({ message: "Verbose" })
        const flag = Flag.boolean("verbose").pipe(Flag.withFallbackPrompt(prompt))

        const [, value] = yield* flag.parse({
          flags: {},
          arguments: []
        })

        assert.strictEqual(value, false)
      }).pipe(Effect.provide(TestLayer)))

    it.effect("returns MissingOption when prompt is cancelled", () =>
      Effect.gen(function*() {
        const prompt = Prompt.text({ message: "Name" })
        const flag = Flag.string("name").pipe(Flag.withFallbackPrompt(prompt))

        yield* MockTerminal.inputKey("c", { ctrl: true })

        const error = yield* Effect.flip(
          flag.parse({
            flags: {},
            arguments: []
          })
        )

        assert.instanceOf(error, CliError.MissingOption)
      }).pipe(Effect.provide(TestLayer)))

    it.effect("returns MissingArgument when argument prompt is cancelled", () =>
      Effect.gen(function*() {
        const prompt = Prompt.text({ message: "File" })
        const argument = Argument.string("file").pipe(Argument.withFallbackPrompt(prompt))

        yield* MockTerminal.inputKey("c", { ctrl: true })

        const error = yield* Effect.flip(
          argument.parse({
            flags: {},
            arguments: []
          })
        )

        assert.instanceOf(error, CliError.MissingArgument)
      }).pipe(Effect.provide(TestLayer)))
  })

  describe("withFallbackConfig", () => {
    it.effect("uses ConfigProvider when a flag is missing", () => {
      const provider = ConfigProvider.fromEnv({
        env: {
          NAME: "Ava"
        }
      })

      return Effect.gen(function*() {
        const flag = Flag.string("name").pipe(
          Flag.withFallbackConfig(Config.string("NAME"))
        )

        const [, value] = yield* flag.parse({
          flags: {},
          arguments: []
        })

        assert.strictEqual(value, "Ava")
      }).pipe(
        Effect.provideService(ConfigProvider.ConfigProvider, provider),
        Effect.provide(TestLayer)
      )
    })

    it.effect("uses flag values before reading config fallbacks", () => {
      const provider = ConfigProvider.fromEnv({
        env: {
          NAME: "Ava"
        }
      })

      return Effect.gen(function*() {
        const flag = Flag.string("name").pipe(
          Flag.withFallbackConfig(Config.string("NAME"))
        )

        const [, value] = yield* flag.parse({
          flags: { name: ["Maya"] },
          arguments: []
        })

        assert.strictEqual(value, "Maya")
      }).pipe(
        Effect.provideService(ConfigProvider.ConfigProvider, provider),
        Effect.provide(TestLayer)
      )
    })

    it.effect("uses ConfigProvider when an argument is missing", () => {
      const provider = ConfigProvider.fromEnv({
        env: {
          REPOSITORY: "repo"
        }
      })

      return Effect.gen(function*() {
        const argument = Argument.string("repository").pipe(
          Argument.withFallbackConfig(Config.string("REPOSITORY"))
        )

        const [, value] = yield* argument.parse({
          flags: {},
          arguments: []
        })

        assert.strictEqual(value, "repo")
      }).pipe(
        Effect.provideService(ConfigProvider.ConfigProvider, provider),
        Effect.provide(TestLayer)
      )
    })

    it.effect("returns MissingOption when config is missing", () => {
      const provider = ConfigProvider.fromEnv({ env: {} })

      return Effect.gen(function*() {
        const flag = Flag.string("name").pipe(
          Flag.withFallbackConfig(Config.string("NAME"))
        )

        const error = yield* Effect.flip(
          flag.parse({
            flags: {},
            arguments: []
          })
        )

        assert.instanceOf(error, CliError.MissingOption)
      }).pipe(
        Effect.provideService(ConfigProvider.ConfigProvider, provider),
        Effect.provide(TestLayer)
      )
    })

    it.effect("returns InvalidValue when config fails to parse", () => {
      const provider = ConfigProvider.fromEnv({
        env: {
          COUNT: "nope"
        }
      })

      return Effect.gen(function*() {
        const flag = Flag.integer("count").pipe(
          Flag.withFallbackConfig(Config.int("COUNT"))
        )

        const error = yield* Effect.flip(
          flag.parse({
            flags: {},
            arguments: []
          })
        )

        assert.instanceOf(error, CliError.InvalidValue)
      }).pipe(
        Effect.provideService(ConfigProvider.ConfigProvider, provider),
        Effect.provide(TestLayer)
      )
    })
  })
})
