import { assert, describe, it } from "@effect/vitest"
import { Effect, Equal, Hash, HashSet, TxHashSet } from "effect"

class TestValue implements Equal.Equal {
  constructor(readonly value: string) {}

  [Equal.symbol](other: unknown): boolean {
    return other instanceof TestValue && this.value === other.value
  }

  [Hash.symbol](): number {
    return Hash.string(this.value)
  }

  toString(): string {
    return `TestValue(${this.value})`
  }
}

describe("TxHashSet", () => {
  describe("constructors", () => {
    it.effect("empty creates an empty TxHashSet", () =>
      Effect.tx(Effect.gen(function*() {
        const txSet = yield* TxHashSet.empty<string>()
        const size = yield* TxHashSet.size(txSet)
        const isEmpty = yield* TxHashSet.isEmpty(txSet)

        assert.strictEqual(size, 0)
        assert.strictEqual(isEmpty, true)
      })))

    it.effect("make creates TxHashSet from values", () =>
      Effect.tx(Effect.gen(function*() {
        const txSet = yield* TxHashSet.make("a", "b", "c")
        const size = yield* TxHashSet.size(txSet)

        assert.strictEqual(size, 3)
        assert.strictEqual(yield* TxHashSet.has(txSet, "a"), true)
        assert.strictEqual(yield* TxHashSet.has(txSet, "b"), true)
        assert.strictEqual(yield* TxHashSet.has(txSet, "c"), true)
        assert.strictEqual(yield* TxHashSet.has(txSet, "d"), false)
      })))

    it.effect("make removes duplicates", () =>
      Effect.tx(Effect.gen(function*() {
        const txSet = yield* TxHashSet.make("a", "b", "a", "c", "b")
        const size = yield* TxHashSet.size(txSet)

        assert.strictEqual(size, 3)
      })))

    it.effect("fromIterable creates TxHashSet from iterable", () =>
      Effect.tx(Effect.gen(function*() {
        const txSet = yield* TxHashSet.fromIterable(["x", "y", "z", "x", "y"])
        const size = yield* TxHashSet.size(txSet)

        assert.strictEqual(size, 3)
        assert.strictEqual(yield* TxHashSet.has(txSet, "x"), true)
        assert.strictEqual(yield* TxHashSet.has(txSet, "y"), true)
        assert.strictEqual(yield* TxHashSet.has(txSet, "z"), true)
      })))

    it.effect("fromHashSet creates TxHashSet from HashSet", () =>
      Effect.tx(Effect.gen(function*() {
        const hashSet = HashSet.make("foo", "bar", "baz")
        const txSet = yield* TxHashSet.fromHashSet(hashSet)
        const size = yield* TxHashSet.size(txSet)

        assert.strictEqual(size, 3)
        assert.strictEqual(yield* TxHashSet.has(txSet, "foo"), true)
        assert.strictEqual(yield* TxHashSet.has(txSet, "bar"), true)
        assert.strictEqual(yield* TxHashSet.has(txSet, "baz"), true)
      })))
  })

  describe("type guards", () => {
    it.effect("isTxHashSet identifies TxHashSet instances", () =>
      Effect.tx(Effect.gen(function*() {
        const txSet = yield* TxHashSet.make(1, 2, 3)
        const hashSet = HashSet.make(1, 2, 3)
        const array = [1, 2, 3]

        assert.strictEqual(TxHashSet.isTxHashSet(txSet), true)
        assert.strictEqual(TxHashSet.isTxHashSet(hashSet), false)
        assert.strictEqual(TxHashSet.isTxHashSet(array), false)
        assert.strictEqual(TxHashSet.isTxHashSet(null), false)
        assert.strictEqual(TxHashSet.isTxHashSet(undefined), false)
      })))
  })

  describe("basic operations", () => {
    it.effect("add adds values to TxHashSet", () =>
      Effect.tx(Effect.gen(function*() {
        const txSet = yield* TxHashSet.make("a", "b")

        yield* TxHashSet.add(txSet, "c")
        assert.strictEqual(yield* TxHashSet.size(txSet), 3)
        assert.strictEqual(yield* TxHashSet.has(txSet, "c"), true)

        // Adding existing value has no effect
        yield* TxHashSet.add(txSet, "a")
        assert.strictEqual(yield* TxHashSet.size(txSet), 3)
      })))

    it.effect("remove removes values from TxHashSet", () =>
      Effect.tx(Effect.gen(function*() {
        const txSet = yield* TxHashSet.make("a", "b", "c")

        const removed = yield* TxHashSet.remove(txSet, "b")
        assert.strictEqual(removed, true)
        assert.strictEqual(yield* TxHashSet.size(txSet), 2)
        assert.strictEqual(yield* TxHashSet.has(txSet, "b"), false)

        // Removing non-existent value returns false
        const notRemoved = yield* TxHashSet.remove(txSet, "d")
        assert.strictEqual(notRemoved, false)
        assert.strictEqual(yield* TxHashSet.size(txSet), 2)
      })))

    it.effect("has checks for value existence", () =>
      Effect.tx(Effect.gen(function*() {
        const txSet = yield* TxHashSet.make("apple", "banana", "cherry")

        assert.strictEqual(yield* TxHashSet.has(txSet, "apple"), true)
        assert.strictEqual(yield* TxHashSet.has(txSet, "grape"), false)
      })))

    it.effect("size returns correct count", () =>
      Effect.tx(Effect.gen(function*() {
        const empty = yield* TxHashSet.empty<string>()
        assert.strictEqual(yield* TxHashSet.size(empty), 0)

        const small = yield* TxHashSet.make("a", "b")
        assert.strictEqual(yield* TxHashSet.size(small), 2)

        const fromIterable = yield* TxHashSet.fromIterable(["x", "y", "z", "x", "y"])
        assert.strictEqual(yield* TxHashSet.size(fromIterable), 3)
      })))

    it.effect("isEmpty checks if set is empty", () =>
      Effect.tx(Effect.gen(function*() {
        const empty = yield* TxHashSet.empty<string>()
        assert.strictEqual(yield* TxHashSet.isEmpty(empty), true)

        const nonEmpty = yield* TxHashSet.make("a")
        assert.strictEqual(yield* TxHashSet.isEmpty(nonEmpty), false)
      })))

    it.effect("clear removes all values", () =>
      Effect.tx(Effect.gen(function*() {
        const txSet = yield* TxHashSet.make("a", "b", "c")
        assert.strictEqual(yield* TxHashSet.size(txSet), 3)

        yield* TxHashSet.clear(txSet)
        assert.strictEqual(yield* TxHashSet.size(txSet), 0)
        assert.strictEqual(yield* TxHashSet.isEmpty(txSet), true)
      })))
  })

  describe("set operations", () => {
    it.effect("union combines two sets", () =>
      Effect.tx(Effect.gen(function*() {
        const set1 = yield* TxHashSet.make("a", "b")
        const set2 = yield* TxHashSet.make("b", "c")
        const combined = yield* TxHashSet.union(set1, set2)

        const values = yield* TxHashSet.toHashSet(combined)
        const sortedValues = Array.from(values).sort()
        assert.deepStrictEqual(sortedValues, ["a", "b", "c"])
        assert.strictEqual(yield* TxHashSet.size(combined), 3)
      })))

    it.effect("intersection finds common elements", () =>
      Effect.tx(Effect.gen(function*() {
        const set1 = yield* TxHashSet.make("a", "b", "c")
        const set2 = yield* TxHashSet.make("b", "c", "d")
        const common = yield* TxHashSet.intersection(set1, set2)

        const values = yield* TxHashSet.toHashSet(common)
        const sortedValues = Array.from(values).sort()
        assert.deepStrictEqual(sortedValues, ["b", "c"])
        assert.strictEqual(yield* TxHashSet.size(common), 2)
      })))

    it.effect("difference finds elements only in first set", () =>
      Effect.tx(Effect.gen(function*() {
        const set1 = yield* TxHashSet.make("a", "b", "c")
        const set2 = yield* TxHashSet.make("b", "d")
        const diff = yield* TxHashSet.difference(set1, set2)

        const values = yield* TxHashSet.toHashSet(diff)
        const sortedValues = Array.from(values).sort()
        assert.deepStrictEqual(sortedValues, ["a", "c"])
        assert.strictEqual(yield* TxHashSet.size(diff), 2)
      })))

    it.effect("isSubset checks subset relationship", () =>
      Effect.tx(Effect.gen(function*() {
        const small = yield* TxHashSet.make("a", "b")
        const large = yield* TxHashSet.make("a", "b", "c", "d")
        const other = yield* TxHashSet.make("x", "y")

        assert.strictEqual(yield* TxHashSet.isSubset(small, large), true)
        assert.strictEqual(yield* TxHashSet.isSubset(large, small), false)
        assert.strictEqual(yield* TxHashSet.isSubset(small, other), false)
        assert.strictEqual(yield* TxHashSet.isSubset(small, small), true)
      })))
  })

  describe("query operations", () => {
    it.effect("some tests if any element satisfies predicate", () =>
      Effect.tx(Effect.gen(function*() {
        const numbers = yield* TxHashSet.make(1, 2, 3, 4, 5)

        assert.strictEqual(yield* TxHashSet.some(numbers, (n) => n > 3), true)
        assert.strictEqual(yield* TxHashSet.some(numbers, (n) => n > 10), false)

        const empty = yield* TxHashSet.empty<number>()
        assert.strictEqual(yield* TxHashSet.some(empty, (n) => n > 0), false)
      })))

    it.effect("every tests if all elements satisfy predicate", () =>
      Effect.tx(Effect.gen(function*() {
        const numbers = yield* TxHashSet.make(2, 4, 6, 8)

        assert.strictEqual(yield* TxHashSet.every(numbers, (n) => n % 2 === 0), true)
        assert.strictEqual(yield* TxHashSet.every(numbers, (n) => n > 5), false)

        const empty = yield* TxHashSet.empty<number>()
        assert.strictEqual(yield* TxHashSet.every(empty, (n) => n > 0), true) // vacuously true
      })))
  })

  describe("functional operations", () => {
    it.effect("map transforms values", () =>
      Effect.tx(Effect.gen(function*() {
        const numbers = yield* TxHashSet.make(1, 2, 3)
        const doubled = yield* TxHashSet.map(numbers, (n) => n * 2)

        const values = yield* TxHashSet.toHashSet(doubled)
        const sortedValues = Array.from(values).sort()
        assert.deepStrictEqual(sortedValues, [2, 4, 6])
        assert.strictEqual(yield* TxHashSet.size(doubled), 3)
      })))

    it.effect("map can reduce size with duplicate results", () =>
      Effect.tx(Effect.gen(function*() {
        const strings = yield* TxHashSet.make("apple", "banana", "cherry")
        const lengths = yield* TxHashSet.map(strings, (s) => s.length)

        const values = yield* TxHashSet.toHashSet(lengths)
        const sortedValues = Array.from(values).sort()
        assert.deepStrictEqual(sortedValues, [5, 6]) // apple=5, banana=6, cherry=6
      })))

    it.effect("filter keeps only matching values", () =>
      Effect.tx(Effect.gen(function*() {
        const numbers = yield* TxHashSet.make(1, 2, 3, 4, 5, 6)
        const evens = yield* TxHashSet.filter(numbers, (n) => n % 2 === 0)

        const values = yield* TxHashSet.toHashSet(evens)
        const sortedValues = Array.from(values).sort()
        assert.deepStrictEqual(sortedValues, [2, 4, 6])
        assert.strictEqual(yield* TxHashSet.size(evens), 3)
      })))

    it.effect("reduce accumulates values", () =>
      Effect.tx(Effect.gen(function*() {
        const numbers = yield* TxHashSet.make(1, 2, 3, 4, 5)
        const sum = yield* TxHashSet.reduce(numbers, 0, (acc, n) => acc + n)

        assert.strictEqual(sum, 15)

        const strings = yield* TxHashSet.make("a", "b", "c")
        const concatenated = yield* TxHashSet.reduce(strings, "", (acc, s) => acc + s)

        // Order may vary, but all letters should be present
        assert.strictEqual(concatenated.length, 3)
        assert.strictEqual(concatenated.includes("a"), true)
        assert.strictEqual(concatenated.includes("b"), true)
        assert.strictEqual(concatenated.includes("c"), true)
      })))
  })

  describe("conversions", () => {
    it.effect("toHashSet creates immutable snapshot", () =>
      Effect.tx(Effect.gen(function*() {
        const txSet = yield* TxHashSet.make("x", "y", "z")
        const hashSet = yield* TxHashSet.toHashSet(txSet)

        assert.strictEqual(HashSet.size(hashSet), 3)
        assert.strictEqual(HashSet.has(hashSet, "y"), true)

        // hashSet is a snapshot - modifications to txSet don't affect it
        yield* TxHashSet.add(txSet, "w")
        assert.strictEqual(HashSet.size(hashSet), 3) // unchanged
        assert.strictEqual(yield* TxHashSet.size(txSet), 4)
      })))
  })

  describe("custom Equal objects", () => {
    it.effect("works with custom Equal implementations", () =>
      Effect.tx(Effect.gen(function*() {
        const txSet = yield* TxHashSet.make(
          new TestValue("alice"),
          new TestValue("bob"),
          new TestValue("charlie")
        )

        assert.strictEqual(yield* TxHashSet.size(txSet), 3)
        assert.strictEqual(yield* TxHashSet.has(txSet, new TestValue("alice")), true)
        assert.strictEqual(yield* TxHashSet.has(txSet, new TestValue("dave")), false)

        // Adding duplicate based on Equal should have no effect
        yield* TxHashSet.add(txSet, new TestValue("alice"))
        assert.strictEqual(yield* TxHashSet.size(txSet), 3)

        // Removing should work with Equal
        const removed = yield* TxHashSet.remove(txSet, new TestValue("bob"))
        assert.strictEqual(removed, true)
        assert.strictEqual(yield* TxHashSet.size(txSet), 2)
        assert.strictEqual(yield* TxHashSet.has(txSet, new TestValue("bob")), false)
      })))
  })

  describe("transactional behavior", () => {
    it.effect("operations are atomic within transactions", () =>
      Effect.tx(Effect.gen(function*() {
        const txSet = yield* TxHashSet.make("a", "b", "c")

        const hasCherry = yield* TxHashSet.has(txSet, "c")
        if (hasCherry) {
          yield* TxHashSet.remove(txSet, "c")
          yield* TxHashSet.add(txSet, "orange")
        }
        yield* TxHashSet.add(txSet, "grape")

        assert.strictEqual(yield* TxHashSet.size(txSet), 4)
        assert.strictEqual(yield* TxHashSet.has(txSet, "c"), false)
        assert.strictEqual(yield* TxHashSet.has(txSet, "orange"), true)
        assert.strictEqual(yield* TxHashSet.has(txSet, "grape"), true)
      })))

    it.effect("multiple set operations work together", () =>
      Effect.tx(Effect.gen(function*() {
        const set1 = yield* TxHashSet.make("a", "b", "c")
        const set2 = yield* TxHashSet.make("c", "d", "e")

        yield* TxHashSet.add(set1, "f")
        yield* TxHashSet.remove(set2, "d")

        const intersection = yield* TxHashSet.intersection(set1, set2)
        const commonSize = yield* TxHashSet.size(intersection)

        if (commonSize > 0) {
          yield* TxHashSet.add(set1, "shared")
          yield* TxHashSet.add(set2, "shared")
        }

        assert.strictEqual(yield* TxHashSet.has(set1, "f"), true)
        assert.strictEqual(yield* TxHashSet.has(set1, "shared"), true)
        assert.strictEqual(yield* TxHashSet.has(set2, "d"), false)
        assert.strictEqual(yield* TxHashSet.has(set2, "shared"), true)
      })))
  })

  describe("stress testing", () => {
    it.effect("handles large number of operations", () =>
      Effect.tx(Effect.gen(function*() {
        const txSet = yield* TxHashSet.empty<number>()

        // Add 1000 numbers
        for (let i = 0; i < 1000; i++) {
          yield* TxHashSet.add(txSet, i)
        }

        assert.strictEqual(yield* TxHashSet.size(txSet), 1000)

        // Check random elements
        assert.strictEqual(yield* TxHashSet.has(txSet, 500), true)
        assert.strictEqual(yield* TxHashSet.has(txSet, 999), true)
        assert.strictEqual(yield* TxHashSet.has(txSet, 1000), false)

        // Remove half the elements
        for (let i = 0; i < 500; i++) {
          yield* TxHashSet.remove(txSet, i)
        }

        assert.strictEqual(yield* TxHashSet.size(txSet), 500)
        assert.strictEqual(yield* TxHashSet.has(txSet, 250), false)
        assert.strictEqual(yield* TxHashSet.has(txSet, 750), true)
      })))

    it.effect("functional operations work with large sets", () =>
      Effect.tx(Effect.gen(function*() {
        const numbers = yield* TxHashSet.fromIterable(Array.from({ length: 100 }, (_, i) => i))

        const evens = yield* TxHashSet.filter(numbers, (n) => n % 2 === 0)
        assert.strictEqual(yield* TxHashSet.size(evens), 50)

        const doubled = yield* TxHashSet.map(evens, (n) => n * 2)
        assert.strictEqual(yield* TxHashSet.size(doubled), 50)

        const sum = yield* TxHashSet.reduce(evens, 0, (acc, n) => acc + n)
        assert.strictEqual(sum, 2450) // sum of even numbers 0 to 98
      })))
  })

  describe("edge cases", () => {
    it.effect("handles empty set operations correctly", () =>
      Effect.tx(Effect.gen(function*() {
        const empty1 = yield* TxHashSet.empty<string>()
        const empty2 = yield* TxHashSet.empty<string>()
        const nonEmpty = yield* TxHashSet.make("a", "b")

        const emptyUnion = yield* TxHashSet.union(empty1, empty2)
        assert.strictEqual(yield* TxHashSet.size(emptyUnion), 0)

        const nonEmptyUnion = yield* TxHashSet.union(empty1, nonEmpty)
        assert.strictEqual(yield* TxHashSet.size(nonEmptyUnion), 2)

        const emptyIntersection = yield* TxHashSet.intersection(empty1, nonEmpty)
        assert.strictEqual(yield* TxHashSet.size(emptyIntersection), 0)

        assert.strictEqual(yield* TxHashSet.isSubset(empty1, nonEmpty), true)
        assert.strictEqual(yield* TxHashSet.isSubset(nonEmpty, empty1), false)
      })))

    it.effect("handles single element sets", () =>
      Effect.tx(Effect.gen(function*() {
        const single = yield* TxHashSet.make("only")

        assert.strictEqual(yield* TxHashSet.size(single), 1)
        assert.strictEqual(yield* TxHashSet.has(single, "only"), true)
        assert.strictEqual(yield* TxHashSet.isEmpty(single), false)

        yield* TxHashSet.clear(single)
        assert.strictEqual(yield* TxHashSet.size(single), 0)
        assert.strictEqual(yield* TxHashSet.isEmpty(single), true)
      })))
  })
})
