import { assert, describe, it } from "@effect/vitest"
import * as Cause from "effect/Cause"
import * as Effect from "effect/Effect"
import * as Random from "effect/Random"

describe("Random", () => {
  describe("next", () => {
    it.effect("generates a number between 0 inclusive and 1 exclusive", () =>
      Effect.gen(function*() {
        const value = yield* Random.next

        assert.isAtLeast(value, 0)
        assert.isBelow(value, 1)
      }))
  })

  describe("nextBoolean", () => {
    it.effect("generates a boolean", () =>
      Effect.gen(function*() {
        const value = yield* Random.nextBoolean

        assert.isTrue(value === true || value === false)
      }))

    it.effect("is deterministic with the same seed", () =>
      Effect.gen(function*() {
        const program = Effect.all([Random.nextBoolean, Random.nextBoolean, Random.nextBoolean])

        const result1 = yield* program.pipe(Random.withSeed("next-boolean-seed"))
        const result2 = yield* program.pipe(Random.withSeed("next-boolean-seed"))

        assert.deepStrictEqual(result1, result2)
      }))

    it.effect("uses a strict greater-than threshold", () =>
      Effect.gen(function*() {
        const values = [0.5, 0.500001, 0.1]
        let index = 0

        const result = yield* Effect.all([Random.nextBoolean, Random.nextBoolean, Random.nextBoolean]).pipe(
          Effect.provideService(Random.Random, {
            nextIntUnsafe: () => 0,
            nextDoubleUnsafe: () => values[index++] ?? 0
          })
        )

        assert.deepStrictEqual(result, [false, true, false])
      }))
  })

  describe("nextInt", () => {
    it.effect("generates a safe integer", () =>
      Effect.gen(function*() {
        const value = yield* Random.nextInt

        assert.isTrue(Number.isSafeInteger(value))
        assert.isAtLeast(value, Number.MIN_SAFE_INTEGER)
        assert.isAtMost(value, Number.MAX_SAFE_INTEGER)
      }))
  })

  describe("nextBetween", () => {
    it.effect("generates number in half-open range", () =>
      Effect.gen(function*() {
        for (let i = 0; i < 100; i++) {
          const value = yield* Random.nextBetween(10, 20)
          assert.isAtLeast(value, 10)
          assert.isBelow(value, 20)
        }
      }))

    it.effect("does not round the bounds", () =>
      Effect.gen(function*() {
        const value = yield* Random.nextBetween(10.5, 20.5).pipe(
          Effect.provideService(Random.Random, {
            nextIntUnsafe: () => 0,
            nextDoubleUnsafe: () => 0.75
          })
        )

        assert.strictEqual(value, 18)
      }))

    it.effect("handles negative ranges", () =>
      Effect.gen(function*() {
        const value = yield* Random.nextBetween(-10, 10)

        assert.isAtLeast(value, -10)
        assert.isBelow(value, 10)
      }))
  })

  describe("nextIntBetween", () => {
    it.effect("generates integer in closed range", () =>
      Effect.gen(function*() {
        for (let i = 0; i < 100; i++) {
          const value = yield* Random.nextIntBetween(1, 6)
          assert.isTrue(Number.isInteger(value))
          assert.isAtLeast(value, 1)
          assert.isAtMost(value, 6)
        }
      }))

    it.effect("generates integer in half-open range", () =>
      Effect.gen(function*() {
        for (let i = 0; i < 100; i++) {
          const value = yield* Random.nextIntBetween(1, 6, {
            halfOpen: true
          })
          assert.isTrue(Number.isInteger(value))
          assert.isAtLeast(value, 1)
          assert.isBelow(value, 6)
        }
      }))

    it.effect("excludes the upper bound in half-open ranges", () =>
      Effect.gen(function*() {
        const value = yield* Random.nextIntBetween(1, 6, { halfOpen: true }).pipe(
          Effect.provideService(Random.Random, {
            nextIntUnsafe: () => 0,
            nextDoubleUnsafe: () => 1 - Number.EPSILON
          })
        )

        assert.strictEqual(value, 5)
      }))
  })

  describe("shuffle", () => {
    it.effect("returns a shuffled copy of all values", () =>
      Effect.gen(function*() {
        const input = [1, 2, 3, 4, 5]
        const output = yield* Random.shuffle(input)

        assert.notStrictEqual(output, input)
        assert.deepStrictEqual(input, [1, 2, 3, 4, 5])
        assert.deepStrictEqual(Array.from(output).sort((a, b) => a - b), [1, 2, 3, 4, 5])
      }))

    it.effect("is deterministic with the same seed", () =>
      Effect.gen(function*() {
        const program = Random.shuffle([1, 2, 3, 4, 5])

        const result1 = yield* program.pipe(Random.withSeed("shuffle-seed"))
        const result2 = yield* program.pipe(Random.withSeed("shuffle-seed"))

        assert.deepStrictEqual(result1, result2)
      }))
  })

  describe("choice", () => {
    it.effect("selects a random element from an iterable", () =>
      Effect.gen(function*() {
        const value = yield* Random.choice(["a", "b", "c"]).pipe(
          Effect.provideService(Random.Random, {
            nextIntUnsafe: () => 0,
            nextDoubleUnsafe: () => 0.75
          })
        )

        assert.strictEqual(value, "c")
      }))

    it.effect("fails with NoSuchElementError for an empty iterable", () =>
      Effect.gen(function*() {
        const error = yield* Random.choice([]).pipe(Effect.flip)

        assert.isTrue(Cause.isNoSuchElementError(error))
        assert.strictEqual(error.message, "Cannot select a random element from an empty array")
      }))

    it.effect("is deterministic with the same seed", () =>
      Effect.gen(function*() {
        const program = Effect.all([
          Random.choice([1, 2, 3, 4, 5]),
          Random.choice([1, 2, 3, 4, 5]),
          Random.choice([1, 2, 3, 4, 5])
        ])

        const result1 = yield* program.pipe(Random.withSeed("choice-seed"))
        const result2 = yield* program.pipe(Random.withSeed("choice-seed"))

        assert.deepStrictEqual(result1, result2)
      }))
  })

  describe("withSeed", () => {
    it.effect("produces deterministic sequence with same seed", () =>
      Effect.gen(function*() {
        const program = Effect.gen(function*() {
          const v1 = yield* Random.next
          const v2 = yield* Random.nextInt
          return [v1, v2]
        })

        const result1 = yield* program.pipe(Random.withSeed("test-seed"))
        const result2 = yield* program.pipe(Random.withSeed("test-seed"))

        assert.deepStrictEqual(result1, result2)
      }))

    it.effect("produces different sequences with different seeds", () =>
      Effect.gen(function*() {
        const program = Effect.gen(function*() {
          const v1 = yield* Random.next
          const v2 = yield* Random.nextInt
          return [v1, v2]
        })

        const result1 = yield* program.pipe(Random.withSeed(12345))
        const result2 = yield* program.pipe(Random.withSeed(67890))

        assert.notDeepEqual(result1, result2)
      }))

    it.effect("distinguishes one-character string seeds", () =>
      Effect.gen(function*() {
        const program = Effect.all([Random.next, Random.next, Random.next, Random.next, Random.next])

        const empty = yield* program.pipe(Random.withSeed(""))
        const result1 = yield* program.pipe(Random.withSeed("a"))
        const result2 = yield* program.pipe(Random.withSeed("b"))

        assert.notDeepEqual(result1, empty)
        assert.notDeepEqual(result2, empty)
        assert.notDeepEqual(result1, result2)
      }))

    it.effect("distinguishes trailing partial UTF-8 words in string seeds", () =>
      Effect.gen(function*() {
        const program = Effect.all([Random.next, Random.next, Random.next, Random.next, Random.next])
        const pairs = [
          ["1234a", "1234b"],
          ["1234ab", "1234ac"],
          ["1234abc", "1234abd"]
        ] as const

        for (const [seed1, seed2] of pairs) {
          const result1 = yield* program.pipe(Random.withSeed(seed1))
          const result2 = yield* program.pipe(Random.withSeed(seed2))

          assert.notDeepEqual(result1, result2)
        }
      }))

    it.effect("distinguishes astral character string seeds", () =>
      Effect.gen(function*() {
        const program = Effect.all([Random.next, Random.next, Random.next, Random.next, Random.next])

        const empty = yield* program.pipe(Random.withSeed(""))
        const result1 = yield* program.pipe(Random.withSeed("😀"))
        const result2 = yield* program.pipe(Random.withSeed("😁"))
        const result3 = yield* program.pipe(Random.withSeed("🐀"))

        assert.notDeepEqual(result1, empty)
        assert.notDeepEqual(result2, empty)
        assert.notDeepEqual(result1, result2)
        assert.notDeepEqual(result1, result3)
      }))

    it.effect("works with numeric seeds", () =>
      Effect.gen(function*() {
        const program = Effect.gen(function*() {
          const v1 = yield* Random.next
          const v2 = yield* Random.nextInt
          return [v1, v2]
        })

        const result1 = yield* program.pipe(Random.withSeed(12345))
        const result2 = yield* program.pipe(Random.withSeed(12345))

        assert.deepStrictEqual(result1, result2)
      }))
  })
})
