import { assert, describe, it } from "@effect/vitest"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import type * as PlatformError from "effect/PlatformError"
import * as Sink from "effect/Sink"
import * as Stream from "effect/Stream"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

const MockExecutorLayer = Layer.succeed(
  ChildProcessSpawner.ChildProcessSpawner,
  ChildProcessSpawner.make(Effect.fnUntraced(function*(command) {
    // For piped commands, flatten to get the first command info
    let cmd = command
    while (cmd._tag === "PipedCommand") {
      cmd = cmd.left
    }
    const executable = cmd._tag === "StandardCommand"
      ? cmd.command
      : "templated"
    const output = new TextEncoder().encode(`mock output for ${executable}`)
    let isReferenced = true
    const reref = Effect.sync(() => {
      if (!isReferenced) {
        isReferenced = true
      }
    })
    return ChildProcessSpawner.makeHandle({
      pid: ChildProcessSpawner.ProcessId(12345),
      stdin: Sink.forEach<Uint8Array, void, never, never>((_chunk) => Effect.void),
      stdout: Stream.fromIterable([output]),
      stderr: Stream.fromIterable([new TextEncoder().encode("")]),
      all: Stream.fromIterable([output]),
      exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)),
      isRunning: Effect.succeed(false),
      kill: () => Effect.void,
      getInputFd: () => Sink.drain,
      getOutputFd: () => Stream.empty,
      unref: Effect.sync(() => {
        if (isReferenced) {
          isReferenced = false
        }
        return reref
      })
    })
  }))
)

describe("ChildProcess", () => {
  describe("make", () => {
    it("template literal form should create a standard command", () => {
      const cmd = ChildProcess.make`echo hello`
      assert(ChildProcess.isStandardCommand(cmd))
      assert.strictEqual(cmd.command, "echo")
      assert.deepStrictEqual(cmd.args, ["hello"])
    })

    it("array form should create a standard command", () => {
      const cmd = ChildProcess.make("node", ["--version"])
      assert.strictEqual(cmd._tag, "StandardCommand")
      assert.isTrue(ChildProcess.isStandardCommand(cmd))
      assert.strictEqual(cmd.command, "node")
      assert.deepStrictEqual(cmd.args, ["--version"])
    })

    it("options form should pass options to templated command", () => {
      const cmd = ChildProcess.make({ cwd: "/tmp" })`ls -la`
      assert(ChildProcess.isStandardCommand(cmd))
      assert.strictEqual(cmd.options.cwd, "/tmp")
    })

    it("array form with options should work", () => {
      const cmd = ChildProcess.make("git", ["status"], { cwd: "/app" })
      assert.strictEqual(cmd._tag, "StandardCommand")
      assert.strictEqual(cmd.command, "git")
      assert.deepStrictEqual(cmd.args, ["status"])
      assert.strictEqual(cmd.options.cwd, "/app")
    })
  })

  describe("spawn", () => {
    it.effect("should spawn a templated command", () =>
      Effect.gen(function*() {
        const cmd = ChildProcess.make`echo hello`
        const handle = yield* cmd
        assert.strictEqual(handle.pid, ChildProcessSpawner.ProcessId(12345))
      }).pipe(Effect.scoped, Effect.provide(MockExecutorLayer)))

    it.effect("should spawn a standard command", () =>
      Effect.gen(function*() {
        const cmd = ChildProcess.make("node", ["--version"])
        const handle = yield* cmd
        assert.strictEqual(handle.pid, ChildProcessSpawner.ProcessId(12345))
      }).pipe(Effect.scoped, Effect.provide(MockExecutorLayer)))

    it.effect("should return a process handle", () =>
      Effect.gen(function*() {
        const cmd = ChildProcess.make`long-running-process`
        const handle = yield* cmd
        assert.strictEqual(handle.pid, ChildProcessSpawner.ProcessId(12345))
      }).pipe(Effect.scoped, Effect.provide(MockExecutorLayer)))

    it.effect("collects stdout through Stream APIs", () =>
      Effect.gen(function*() {
        const cmd = ChildProcess.make("cat", ["file.txt"])
        const handle = yield* cmd
        const chunks = yield* Stream.runCollect(handle.stdout)
        assert.isTrue(chunks.length > 0)
      }).pipe(Effect.scoped, Effect.provide(MockExecutorLayer)))

    it.effect("should allow waiting for exit code", () =>
      Effect.gen(function*() {
        const cmd = ChildProcess.make`echo test`
        const handle = yield* cmd
        const exitCode = yield* handle.exitCode
        assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0))
      }).pipe(Effect.scoped, Effect.provide(MockExecutorLayer)))

    it.effect("should unref a process and return a reref effect", () =>
      Effect.gen(function*() {
        const cmd = ChildProcess.make`echo test`
        const handle = yield* cmd
        const reref = yield* handle.unref
        assert.isDefined(reref)
        yield* reref
      }).pipe(Effect.scoped, Effect.provide(MockExecutorLayer)))

    it.effect("should allow restoring the reference within acquireRelease", () =>
      Effect.gen(function*() {
        const handle = yield* ChildProcess.make`echo test`
        // @effect-diagnostics-next-line floatingEffect:off
        yield* Effect.acquireRelease(handle.unref, (reref) => Effect.ignore(reref))
      }).pipe(Effect.provide(MockExecutorLayer)))
  })

  describe("pipeTo", () => {
    it("should create a piped command", () => {
      const pipeline = ChildProcess.make`cat file.txt`.pipe(
        ChildProcess.pipeTo(ChildProcess.make`grep pattern`),
        ChildProcess.pipeTo(ChildProcess.make`wc -l`)
      )
      assert.strictEqual(pipeline._tag, "PipedCommand")
    })

    it.effect("should allow spawning a pipeline", () =>
      Effect.gen(function*() {
        const pipeline = ChildProcess.make`cat file.txt`.pipe(
          ChildProcess.pipeTo(ChildProcess.make`grep pattern`)
        )
        const handle = yield* pipeline
        assert.strictEqual(handle.pid, ChildProcessSpawner.ProcessId(12345))
      }).pipe(Effect.scoped, Effect.provide(MockExecutorLayer)))
  })

  describe("setCwd", () => {
    it("should set cwd on standard commands", () => {
      const standard = ChildProcess.make("ls", ["-la"], { env: { TEST: "1" } })
      const updatedStandard = ChildProcess.setCwd(standard, "/tmp")
      assert(updatedStandard._tag === "StandardCommand")
      assert.strictEqual(updatedStandard.options.cwd, "/tmp")
      assert.deepStrictEqual(updatedStandard.options.env, { TEST: "1" })
    })

    it("should set cwd on all commands in a pipeline", () => {
      const pipeline = ChildProcess.make`cat file`.pipe(
        ChildProcess.pipeTo(ChildProcess.make`grep pattern`)
      )
      const updated = ChildProcess.setCwd(pipeline, "/tmp")
      assert.strictEqual(updated._tag, "PipedCommand")
      if (updated._tag === "PipedCommand") {
        const left = updated.left
        const right = updated.right
        assert(left._tag === "StandardCommand")
        assert.strictEqual(left.options.cwd, "/tmp")
        assert(right._tag === "StandardCommand")
        assert.strictEqual(right.options.cwd, "/tmp")
      }
    })
  })

  describe("prefix", () => {
    it("should prefix a standard command and preserve options", () => {
      const command = ChildProcess.make("echo", ["hello"], { cwd: "/tmp", env: { TEST: "1" } })
      const prefixed = command.pipe(ChildProcess.prefix`time`)
      assert(prefixed._tag === "StandardCommand")
      assert.strictEqual(prefixed.command, "time")
      assert.deepStrictEqual(prefixed.args, ["echo", "hello"])
      assert.strictEqual(prefixed.options.cwd, "/tmp")
      assert.deepStrictEqual(prefixed.options.env, { TEST: "1" })
    })

    it("should prefix the leftmost command in a pipeline", () => {
      const pipeline = ChildProcess.make`cat file`.pipe(
        ChildProcess.pipeTo(ChildProcess.make`grep pattern`),
        ChildProcess.pipeTo(ChildProcess.make`wc -l`, { from: "stderr" })
      )

      const prefixed = pipeline.pipe(ChildProcess.prefix`time`)
      assert(prefixed._tag === "PipedCommand")
      assert.strictEqual(prefixed.options.from, "stderr")

      const left = prefixed.left
      assert(left._tag === "PipedCommand")

      const leftmost = left.left
      assert(leftmost._tag === "StandardCommand")
      assert.strictEqual(leftmost.command, "time")
      assert.deepStrictEqual(leftmost.args, ["cat", "file"])

      const middle = left.right
      assert(middle._tag === "StandardCommand")
      assert.strictEqual(middle.command, "grep")
      assert.deepStrictEqual(middle.args, ["pattern"])

      const right = prefixed.right
      assert(right._tag === "StandardCommand")
      assert.strictEqual(right.command, "wc")
      assert.deepStrictEqual(right.args, ["-l"])
    })
  })

  describe("guards", () => {
    it("isCommand should detect commands", () => {
      const cmd = ChildProcess.make`echo hello`
      assert.isTrue(ChildProcess.isCommand(cmd))
      assert.isFalse(ChildProcess.isCommand({ _tag: "Other" }))
      assert.isFalse(ChildProcess.isCommand(null))
    })

    it("isStandardCommand should detect standard commands", () => {
      const standard = ChildProcess.make("echo", ["hello"])
      const templated = ChildProcess.make`echo hello`
      const piped = ChildProcess.make`cat file`.pipe(
        ChildProcess.pipeTo(ChildProcess.make`grep pattern`)
      )
      assert.isTrue(ChildProcess.isStandardCommand(standard))
      assert.isTrue(ChildProcess.isStandardCommand(templated))
      assert.isFalse(ChildProcess.isStandardCommand(piped))
    })

    it("isPipedCommand should detect piped commands", () => {
      const single = ChildProcess.make`echo hello`
      const piped = ChildProcess.make`cat file`.pipe(
        ChildProcess.pipeTo(ChildProcess.make`grep pattern`)
      )
      assert.isFalse(ChildProcess.isPipedCommand(single))
      assert.isTrue(ChildProcess.isPipedCommand(piped))
    })
  })

  describe("additionalFds", () => {
    describe("parseFdName", () => {
      it("should parse valid fd names", () => {
        assert.strictEqual(ChildProcess.parseFdName("fd3"), 3)
        assert.strictEqual(ChildProcess.parseFdName("fd4"), 4)
        assert.strictEqual(ChildProcess.parseFdName("fd10"), 10)
        assert.strictEqual(ChildProcess.parseFdName("fd100"), 100)
      })

      it("should reject invalid fd names", () => {
        assert.isUndefined(ChildProcess.parseFdName("stdin"))
        assert.isUndefined(ChildProcess.parseFdName("stdout"))
        assert.isUndefined(ChildProcess.parseFdName("stderr"))
        assert.isUndefined(ChildProcess.parseFdName("fd0"))
        assert.isUndefined(ChildProcess.parseFdName("fd1"))
        assert.isUndefined(ChildProcess.parseFdName("fd2"))
        assert.isUndefined(ChildProcess.parseFdName("invalid"))
        assert.isUndefined(ChildProcess.parseFdName("fd"))
        assert.isUndefined(ChildProcess.parseFdName(""))
      })
    })

    describe("fdName", () => {
      it("should create fd names from numbers", () => {
        assert.strictEqual(ChildProcess.fdName(3), "fd3")
        assert.strictEqual(ChildProcess.fdName(4), "fd4")
        assert.strictEqual(ChildProcess.fdName(10), "fd10")
      })
    })

    describe("command options", () => {
      it("should accept additionalFds option with output fd", () => {
        const cmd = ChildProcess.make("my-program", [], {
          additionalFds: { fd3: { type: "output" } }
        })
        assert.strictEqual(cmd._tag, "StandardCommand")
        assert.isDefined(cmd.options.additionalFds)
        assert.strictEqual(cmd.options.additionalFds!.fd3.type, "output")
      })

      it("should accept additionalFds option with input fd", () => {
        const cmd = ChildProcess.make("my-program", [], {
          additionalFds: { fd3: { type: "input" } }
        })
        assert.strictEqual(cmd._tag, "StandardCommand")
        assert.isDefined(cmd.options.additionalFds)
        assert.strictEqual(cmd.options.additionalFds!.fd3.type, "input")
      })

      it("should accept multiple additional fds", () => {
        const cmd = ChildProcess.make("my-program", [], {
          additionalFds: {
            fd3: { type: "output" },
            fd4: { type: "input" },
            fd5: { type: "output" }
          }
        })
        assert.strictEqual(cmd._tag, "StandardCommand")
        assert.isDefined(cmd.options.additionalFds)
        assert.strictEqual(cmd.options.additionalFds!.fd3.type, "output")
        assert.strictEqual(cmd.options.additionalFds!.fd4.type, "input")
        assert.strictEqual(cmd.options.additionalFds!.fd5.type, "output")
      })

      it("should accept additionalFds with streams", () => {
        const inputStream = Stream.fromIterable([new TextEncoder().encode("test")])
        const cmd = ChildProcess.make("my-program", [], {
          additionalFds: { fd3: { type: "input", stream: inputStream } }
        })
        assert.strictEqual(cmd._tag, "StandardCommand")
        assert.isDefined(cmd.options.additionalFds)
        assert.strictEqual(cmd.options.additionalFds!.fd3.type, "input")
        assert.propertyVal(cmd.options.additionalFds!.fd3, "stream", inputStream)
      })

      it("should accept additionalFds with sinks", () => {
        const outputSink: Sink.Sink<
          Uint8Array<ArrayBufferLike>,
          Uint8Array<ArrayBufferLike>,
          never,
          PlatformError.PlatformError
        > = Sink.succeed(new Uint8Array())
        const cmd = ChildProcess.make("my-program", [], {
          additionalFds: { fd3: { type: "output", sink: outputSink } }
        })
        assert.strictEqual(cmd._tag, "StandardCommand")
        assert.isDefined(cmd.options.additionalFds)
        assert.strictEqual(cmd.options.additionalFds!.fd3.type, "output")
        assert.propertyVal(cmd.options.additionalFds!.fd3, "sink", outputSink)
      })
    })

    describe("ChildProcessHandle additionalFds", () => {
      it.effect("should have additionalFds accessor on handle", () =>
        Effect.gen(function*() {
          const cmd = ChildProcess.make`echo hello`
          const handle = yield* cmd
          assert.isDefined(handle.getInputFd)
          assert.isDefined(handle.getOutputFd)
        }).pipe(Effect.scoped, Effect.provide(MockExecutorLayer)))
    })
  })
})
