import { assert, describe, it } from "@effect/vitest"
import { Effect, FileSystem, Layer, Path, PlatformError, Redacted, Stdio } from "effect"
import { TestConsole } from "effect/testing/index"
import { Primitive } 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
)

// Helper functions to reduce repetition
const expectValidValues = <A>(
  primitive: Primitive.Primitive<A>,
  cases: Array<[string, A]>
) =>
  Effect.gen(function*() {
    for (const [input, expected] of cases) {
      const result = yield* primitive.parse(input)
      assert.strictEqual(result, expected)
    }
  })

const expectInvalidValues = <A>(
  primitive: Primitive.Primitive<A>,
  inputs: ReadonlyArray<string>,
  messages: ReadonlyArray<string>
) =>
  Effect.gen(function*() {
    for (let i = 0; i < inputs.length; i++) {
      const error = yield* Effect.flip(primitive.parse(inputs[i]))
      assert.strictEqual(error, messages[i])
    }
  })

const expectValidDates = (
  primitive: Primitive.Primitive<Date>,
  cases: Array<[string, (date: Date) => void]>
) =>
  Effect.gen(function*() {
    for (const [input, validator] of cases) {
      const result = yield* primitive.parse(input)
      assert.isTrue(result instanceof Date)
      validator(result)
    }
  }) as Effect.Effect<void, string, never>

describe("Primitive", () => {
  describe("boolean", () => {
    it.layer(TestLayer)((it) => {
      it.effect("should parse true values correctly", () =>
        expectValidValues(Primitive.boolean, [
          ["true", true],
          ["1", true],
          ["y", true],
          ["yes", true],
          ["on", true]
        ]))

      it.effect("should parse false values correctly", () =>
        expectValidValues(Primitive.boolean, [
          ["false", false],
          ["0", false],
          ["n", false],
          ["no", false],
          ["off", false]
        ]))

      it.effect("should fail for invalid values", () =>
        expectInvalidValues(
          Primitive.boolean,
          ["invalid"],
          [`Expected "true" | "yes" | "on" | "1" | "y" | "false" | "no" | "off" | "0" | "n", got "invalid"`]
        ))

      it("should have correct _tag", () => {
        assert.strictEqual(Primitive.boolean._tag, "Boolean")
      })
    })
  })

  describe("float", () => {
    it.layer(TestLayer)((it) => {
      it.effect("should parse valid float values", () =>
        expectValidValues(Primitive.float, [
          ["42", 42],
          ["3.14", 3.14],
          ["-42.5", -42.5],
          ["0", 0],
          ["1e3", 1000]
        ]))

      it.effect("should fail for invalid values", () =>
        expectInvalidValues(Primitive.float, ["not-a-number"], [
          `Expected a string representing a finite number, got "not-a-number"`
        ]))

      it("should have correct _tag", () => {
        assert.strictEqual(Primitive.float._tag, "Float")
      })
    })
  })

  describe("date", () => {
    it.layer(TestLayer)((it) => {
      it.effect("should parse valid date values", () =>
        expectValidDates(Primitive.date, [
          // ISO date
          [
            "2024-01-15",
            (date) => {
              assert.strictEqual(date.toISOString().slice(0, 10), "2024-01-15")
            }
          ],
          // Full ISO datetime
          [
            "2024-01-15T12:30:45.123Z",
            (date) => {
              assert.strictEqual(date.toISOString(), "2024-01-15T12:30:45.123Z")
            }
          ],
          // With timezone offset
          [
            "2024-01-15T12:30:45+02:00",
            (date) => {
              assert.strictEqual(date.getUTCHours(), 10)
              assert.strictEqual(date.getUTCMinutes(), 30)
            }
          ]
        ]))

      it.effect("should fail for invalid values", () =>
        expectInvalidValues(Primitive.date, ["not-a-date"], [`Expected a valid date, got Invalid Date`]))

      it("should have correct _tag", () => {
        assert.strictEqual(Primitive.date._tag, "Date")
      })
    })
  })

  describe("integer", () => {
    it.layer(TestLayer)((it) => {
      it.effect("should parse valid integer values", () =>
        expectValidValues(Primitive.integer, [
          ["42", 42],
          ["-123", -123],
          ["0", 0],
          ["9007199254740991", 9007199254740991],
          ["1e3", 1000]
        ]))

      it.effect("should fail for invalid values", () =>
        expectInvalidValues(
          Primitive.integer,
          ["3.14", "not-a-number"],
          [`Expected an integer, got 3.14`, `Expected a string representing a finite number, got "not-a-number"`]
        ))

      it("should have correct _tag", () => {
        assert.strictEqual(Primitive.integer._tag, "Integer")
      })
    })
  })

  describe("string", () => {
    it.layer(TestLayer)((it) => {
      it.effect("should parse string values", () =>
        expectValidValues(Primitive.string, [
          ["hello", "hello"],
          ["", ""],
          [" spaces ", " spaces "],
          ["123", "123"],
          ["special!@#$%", "special!@#$%"]
        ]))

      it("should have correct _tag", () => {
        assert.strictEqual(Primitive.string._tag, "String")
      })
    })
  })

  describe("choice", () => {
    const colorChoice = Primitive.choice([
      ["red", "RED"],
      ["green", "GREEN"],
      ["blue", "BLUE"]
    ])

    it.layer(TestLayer)((it) => {
      it.effect("should parse valid choices", () =>
        expectValidValues(colorChoice, [
          ["red", "RED"],
          ["green", "GREEN"],
          ["blue", "BLUE"]
        ]))

      it.effect("should fail for invalid choices", () =>
        expectInvalidValues(
          colorChoice,
          ["yellow", "purple", ""],
          [
            `Expected "red" | "green" | "blue", got "yellow"`,
            `Expected "red" | "green" | "blue", got "purple"`,
            `Expected "red" | "green" | "blue", got ""`
          ]
        ))

      it("should have correct _tag", () => {
        assert.strictEqual(colorChoice._tag, "Choice")
      })

      const numberChoice = Primitive.choice([
        ["one", 1],
        ["two", 2],
        ["three", 3]
      ])

      it.effect("should work with different value types", () =>
        expectValidValues(numberChoice, [
          ["one", 1],
          ["two", 2],
          ["three", 3]
        ]))
    })
  })

  describe("path", () => {
    it.layer(TestLayer)((it) => {
      it.effect("should resolve paths without requiring existence", () =>
        Effect.gen(function*() {
          const pathPrimitive = Primitive.path("either")
          const result1 = yield* pathPrimitive.parse("./test.txt")
          const result2 = yield* pathPrimitive.parse("/absolute/path")
          const result3 = yield* pathPrimitive.parse("relative/path")

          // Results should be absolute paths
          assert.isTrue(result1.includes("test.txt"))
          assert.isTrue(result2 === "/absolute/path")
          assert.isTrue(result3.includes("relative/path"))
        }))

      it("should have correct _tag", () => {
        assert.strictEqual(Primitive.path("either")._tag, "Path")
      })

      it.effect("should fail when a required file path does not exist", () =>
        Effect.gen(function*() {
          const filePath = Primitive.path("file", true)

          // Test non-existent file - should fail validation
          const error = yield* Effect.flip(
            filePath.parse("/non/existent/file.txt")
          )

          assert.strictEqual(error, "Path does not exist: /non/existent/file.txt")
        }).pipe(
          Effect.provide(
            FileSystem.layerNoop({
              stat: (path) =>
                Effect.fail(
                  PlatformError.badArgument({
                    module: "",
                    method: "",
                    description: `The specified path ${path} does not exist`
                  })
                )
            })
          )
        ))

      it.effect("should fail when a required directory path does not exist", () =>
        Effect.gen(function*() {
          const dirPath = Primitive.path("directory", true)

          // Test non-existent directory - should fail validation
          const error = yield* Effect.flip(
            dirPath.parse("/non/existent/directory")
          )

          assert.strictEqual(error, "Path does not exist: /non/existent/directory")
        }).pipe(
          Effect.provide(
            FileSystem.layerNoop({
              stat: (path) =>
                Effect.fail(
                  PlatformError.badArgument({
                    module: "",
                    method: "",
                    description: `The specified path ${path} does not exist`
                  })
                )
            })
          )
        ))
    })
  })

  describe("redacted", () => {
    it.layer(TestLayer)((it) => {
      it.effect("should parse and redact values", () =>
        Effect.gen(function*() {
          const result = yield* Primitive.redacted.parse("secret123")
          // Check if it's a Redacted value
          assert.isTrue(Redacted.isRedacted(result))
          // The toString method should return a redacted representation
          assert.strictEqual(String(result), "<redacted>")
        }))

      it("should have correct _tag", () => {
        assert.strictEqual(Primitive.redacted._tag, "Redacted")
      })

      it.effect("should handle empty strings", () =>
        Effect.gen(function*() {
          const result = yield* Primitive.redacted.parse("")
          assert.isTrue(Redacted.isRedacted(result))
        }))
    })
  })
})
