import { assertFalse, assertNone, assertSome, assertTrue, deepStrictEqual } from "@effect/vitest/utils"
import { Equivalence, Number as Num, Option, Record, Result } from "effect"
import { pipe } from "effect/Function"
import { describe, it } from "vitest"

describe("Record", () => {
  it("singleton", () => {
    deepStrictEqual(Record.singleton("a", 1), { a: 1 })
    deepStrictEqual(Record.singleton("__proto__", 1), { ["__proto__"]: 1 })
  })

  it("makeReducerUnion", () => {
    const R = Record.makeReducerUnion(Num.ReducerSum)

    deepStrictEqual(R.combine({ a: 1, b: 2 }, { a: 3, b: 4, c: 5 }), { a: 4, b: 6, c: 5 })
  })

  it("makeReducerIntersection", () => {
    const R = Record.makeReducerIntersection(Num.ReducerSum)

    deepStrictEqual(R.combine({ a: 1, b: 2 }, { a: 3, b: 4, c: 5 }), { a: 4, b: 6 })
  })
})

const symA = Symbol.for("a")
const symB = Symbol.for("b")
const symC = Symbol.for("c")

const stringRecord: Record<string, number> = { a: 1, [symA]: null }
const symbolRecord: Record<symbol, number> = { [symA]: 1, [symB]: 2 }

describe("Record", () => {
  describe("string | symbol APIs", () => {
    it("empty", () => {
      deepStrictEqual(Record.empty(), {})
    })

    it("fromIterableWith", () => {
      deepStrictEqual(Record.fromIterableWith([1, 2, 3, 4], (a) => [a === 3 ? "a" : String(a), a * 2]), {
        "1": 2,
        "2": 4,
        a: 6,
        "4": 8
      })
      deepStrictEqual(Record.fromIterableWith([1, 2, 3, 4], (a) => [a === 3 ? symA : String(a), a * 2]), {
        "1": 2,
        "2": 4,
        [symA]: 6,
        "4": 8
      })
    })

    it("fromIterableBy", () => {
      const users = [
        { id: "2", name: "name2" },
        { id: "1", name: "name1" }
      ]
      deepStrictEqual(Record.fromIterableBy(users, (user) => user.id), {
        "2": { id: "2", name: "name2" },
        "1": { id: "1", name: "name1" }
      })

      deepStrictEqual(Record.fromIterableBy(["a", symA], (s) => s), { a: "a", [symA]: symA })
    })

    it("fromEntries", () => {
      deepStrictEqual(Record.fromEntries([["1", 2], ["2", 4], ["3", 6], ["4", 8]]), {
        "1": 2,
        "2": 4,
        "3": 6,
        "4": 8
      })
      deepStrictEqual(Record.fromEntries([["1", 2], ["2", 4], ["3", 6], ["4", 8], [symA, 10], [symB, 12]]), {
        "1": 2,
        "2": 4,
        "3": 6,
        "4": 8,
        [symA]: 10,
        [symB]: 12
      })
    })

    it("has", () => {
      assertTrue(Record.has(stringRecord, "a"))
      assertFalse(Record.has(stringRecord, "c"))

      assertTrue(Record.has(symbolRecord, symA))
      assertFalse(Record.has(symbolRecord, symC))
    })

    it("get", () => {
      assertNone(pipe(Record.empty<string>(), Record.get("a")))
      assertSome(pipe(stringRecord, Record.get("a")), 1)

      assertNone(pipe(Record.empty<symbol>(), Record.get(symA)))
      assertSome(pipe(symbolRecord, Record.get(symA)), 1)
    })

    it("modify", () => {
      assertNone(pipe(Record.empty<string>(), Record.modify("a", (n) => n + 1)))
      assertSome(pipe(stringRecord, Record.modify("a", (n: number) => n + 1)), { a: 2, [symA]: null })
      assertSome(pipe(stringRecord, Record.modify("a", (n: number) => String(n))), { a: "1", [symA]: null })

      assertNone(pipe(Record.empty<symbol>(), Record.modify(symA, (n) => n + 1)))
      assertSome(pipe(symbolRecord, Record.modify(symA, (n: number) => n + 1)), { [symA]: 2, [symB]: 2 })
      assertSome(
        pipe(symbolRecord, Record.modify(symA, (n: number) => String(n))),
        { [symA]: "1", [symB]: 2 }
      )
    })

    it("replace", () => {
      assertNone(pipe(Record.empty<string>(), Record.replace("a", 2)))
      assertSome(pipe(stringRecord, Record.replace("a", 2)), { a: 2, [symA]: null })
      assertSome(pipe(stringRecord, Record.replace("a", true)), { a: true, [symA]: null })

      assertNone(pipe(Record.empty<symbol>(), Record.replace(symA, 2)))
      assertSome(pipe(symbolRecord, Record.replace(symA, 2)), { [symA]: 2, [symB]: 2 })
      assertSome(pipe(symbolRecord, Record.replace(symA, true)), { [symA]: true, [symB]: 2 })
    })

    it("remove", () => {
      deepStrictEqual(Record.remove(stringRecord, "a"), { [symA]: null })
      deepStrictEqual(Record.remove(stringRecord, "c"), stringRecord)

      deepStrictEqual(Record.remove(symbolRecord, symA), { [symB]: 2 })
      deepStrictEqual(Record.remove(symbolRecord, symC), symbolRecord)
    })

    describe("pop", () => {
      it("should return the value associated with the given key, if the key is present in the record", () => {
        const result1 = Record.pop(stringRecord, "a")
        assertSome(result1, [1, { [symA]: null }])

        const result2 = Record.pop(symbolRecord, symA)
        assertSome(result2, [1, { [symB]: 2 }])
      })

      it("should return none if the key is not present in the record", () => {
        const result1 = Record.pop(stringRecord, "c")
        assertNone(result1)

        const result2 = Record.pop(symbolRecord, symC)
        assertNone(result2)
      })
    })

    describe("set", () => {
      it("should replace an existing value", () => {
        deepStrictEqual(Record.set(stringRecord, "a", 2), { a: 2, [symA]: null })

        deepStrictEqual(Record.set(symbolRecord, symA, 2), { [symA]: 2, [symB]: 2 })
      })

      it("should add the key / value pair", () => {
        deepStrictEqual(Record.set(stringRecord, "c", 3), { a: 1, [symA]: null, c: 3 })

        deepStrictEqual(Record.set(symbolRecord, symC, 3), { [symA]: 1, [symB]: 2, [symC]: 3 })
      })
    })

    it("singleton", () => {
      deepStrictEqual(Record.singleton("a", 1), { a: 1 })

      deepStrictEqual(Record.singleton(symA, 1), { [symA]: 1 })
    })
  })

  describe("string only APIs", () => {
    it("map", () => {
      deepStrictEqual(pipe(stringRecord, Record.map((n) => n * 2)), { a: 2, [symA]: null })
      deepStrictEqual(pipe(stringRecord, Record.map((n, k) => `${k}-${n}`)), { a: "a-1", [symA]: null })
    })

    it("collect", () => {
      const x = { a: 1, b: 2, c: 3, [symA]: null }
      deepStrictEqual(Record.collect(x, (key, n) => [key, n]), [["a", 1], ["b", 2], ["c", 3]])
    })

    it("toEntries", () => {
      const x = { a: 1, b: 2, c: 3, [symA]: null }
      deepStrictEqual(Record.toEntries(x), [["a", 1], ["b", 2], ["c", 3]])
    })

    it("filterMap", () => {
      const x: Record<string, number> = { a: 1, b: 2, c: 3, [symA]: null }
      const filtered = Record.filterMap(x, (value, key) => (value > 2 ? Result.succeed(key) : Result.failVoid))
      deepStrictEqual(filtered, { c: "c" })
    })

    it("getSomes", () => {
      const x = { a: Option.some(1), b: Option.none(), c: Option.some(2), [symA]: null }
      deepStrictEqual(Record.getSomes(x), { a: 1, c: 2 })
    })

    it("filter", () => {
      const x: Record<string, number> = { a: 1, b: 2, c: 3, d: 4, [symA]: null }
      deepStrictEqual(Record.filter(x, (value) => value > 2), { c: 3, d: 4 })
    })

    it("partition", () => {
      const f = (n: number) => (n > 2 ? Result.succeed(n + 1) : Result.fail(n - 1))
      deepStrictEqual(Record.partition({}, f), [{}, {}])
      deepStrictEqual(Record.partition({ a: 1, b: 3, [symA]: null }, f), [{ a: 0 }, { b: 4 }])
    })

    it("separate", () => {
      deepStrictEqual(
        Record.separate({ a: Result.fail("e"), b: Result.succeed(1), [symA]: null }),
        [{ a: "e" }, { b: 1 }]
      )
      // should ignore non own properties
      const o: Record.ReadonlyRecord<"a", Result.Result<number, string>> = Object.create({ a: 1 })
      deepStrictEqual(pipe(o, Record.separate), [{}, {}])
    })

    it("isEmptyRecord", () => {
      deepStrictEqual(Record.isEmptyRecord({}), true)
      deepStrictEqual(Record.isEmptyRecord({ [symA]: null }), true)
      deepStrictEqual(Record.isEmptyRecord({ a: 3 }), false)
    })

    it("isEmptyReadonlyRecord", () => {
      deepStrictEqual(Record.isEmptyReadonlyRecord({}), true)
      deepStrictEqual(Record.isEmptyReadonlyRecord({ [symA]: null }), true)
      deepStrictEqual(Record.isEmptyReadonlyRecord({ a: 3 }), false)
    })

    it("size", () => {
      deepStrictEqual(Record.size({ a: "a", b: 1, c: true, [symA]: null }), 3)
    })

    it("keys", () => {
      deepStrictEqual(Record.keys({ a: 1, b: 2, [symA]: null }), ["a", "b"])
    })

    it("values", () => {
      deepStrictEqual(Record.values({ a: 1, b: 2, [symA]: null }), [1, 2])
    })

    it("isSubrecord", () => {
      assertTrue(Record.isSubrecord(Record.empty(), {}))
      assertTrue(Record.isSubrecord(Record.empty<string>(), { a: 1 }))
      assertTrue(Record.isSubrecord({ a: 1 }, { a: 1 }))
      assertTrue(Record.isSubrecord(stringRecord, { a: 1 }))
      assertTrue(Record.isSubrecord({ a: 1 }, stringRecord))
      assertTrue(Record.isSubrecord({ a: 1 } as Record<string, number>, { a: 1, b: 2 }))
      assertTrue(Record.isSubrecord({ b: 2, a: 1 }, { a: 1, b: 2 }))
      assertFalse(Record.isSubrecord({ a: 1 }, { a: 2 }))
      assertFalse(Record.isSubrecord({ b: 2 } as Record<string, number>, { a: 1 }))
    })

    it("reduce", () => {
      // data-first
      deepStrictEqual(
        Record.reduce({ k1: "a", k2: "b", [symA]: null }, "-", (accumulator, value, key) => accumulator + key + value),
        "-k1ak2b"
      )
      // data-last
      deepStrictEqual(
        pipe(
          { k1: "a", k2: "b", [symA]: null },
          Record.reduce("-", (accumulator, value, key) => accumulator + key + value)
        ),
        "-k1ak2b"
      )
    })

    it("every", () => {
      assertTrue(Record.every((n: number) => n <= 2)({ a: 1, b: 2, [symA]: null }))
      assertFalse(Record.every((n: number) => n <= 1)({ a: 1, b: 2, [symA]: null }))
    })

    it("some", () => {
      assertTrue(Record.some((n: number) => n <= 1)({ a: 1, b: 2, [symA]: null }))
      assertFalse(Record.some((n: number) => n <= 0)({ a: 1, b: 2, [symA]: null }))
    })

    it("union", () => {
      const combine = (s1: string, s2: string) => s1 + s2
      const x: Record.ReadonlyRecord<string, string> = {
        a: "a1",
        b: "b1",
        c: "c1",
        [symA]: null
      }
      const y: Record.ReadonlyRecord<string, string> = {
        b: "b2",
        c: "c2",
        d: "d2",
        [symA]: null
      }
      deepStrictEqual(Record.union(x, {}, combine), x)
      deepStrictEqual(Record.union({}, x, combine), x)
      deepStrictEqual(Record.union(x, {}, combine), x)
      deepStrictEqual(Record.union({}, x, combine), x)
      deepStrictEqual(Record.union(x, y, combine), {
        a: "a1",
        b: "b1b2",
        c: "c1c2",
        d: "d2"
      })
    })

    it("intersection", () => {
      const combine = (s1: string, s2: string) => s1 + s2
      const x: Record.ReadonlyRecord<string, string> = {
        a: "a1",
        b: "b1",
        c: "c1",
        [symA]: null
      }
      const y: Record.ReadonlyRecord<string, string> = {
        b: "b2",
        c: "c2",
        d: "d2",
        [symA]: null
      }
      deepStrictEqual(Record.intersection(x, {}, combine), {})
      deepStrictEqual(Record.intersection({}, y, combine), {})
      deepStrictEqual(Record.intersection(x, y, combine), {
        b: "b1b2",
        c: "c1c2"
      })
    })

    it("difference", () => {
      const x: Record.ReadonlyRecord<string, string> = {
        a: "a1",
        b: "b1",
        c: "c1",
        [symA]: null
      }
      const y: Record.ReadonlyRecord<string, string> = {
        b: "b2",
        c: "c2",
        d: "d2",
        [symA]: null
      }
      deepStrictEqual(Record.difference({}, x), x)
      deepStrictEqual(Record.difference(x, {}), x)
      deepStrictEqual(Record.difference({}, x), x)
      deepStrictEqual(Record.difference(x, {}), x)
      deepStrictEqual(Record.difference(x, y), {
        a: "a1",
        d: "d2"
      })
    })

    it("makeEquivalence", () => {
      deepStrictEqual(Record.makeEquivalence(Equivalence.strictEqual<number>())({ a: 1 }, { a: 1 }), true)
      deepStrictEqual(Record.makeEquivalence(Equivalence.strictEqual<number>())({ a: 1 }, stringRecord), true)
      deepStrictEqual(Record.makeEquivalence(Equivalence.strictEqual<number>())({ a: 1 }, { a: 2 }), false)
      deepStrictEqual(Record.makeEquivalence(Equivalence.strictEqual<number>())({ a: 1 }, { b: 1 }), false)
      const noPrototypeObject = Object.create(null)
      deepStrictEqual(Record.makeEquivalence(Equivalence.strictEqual<number>())(noPrototypeObject, { b: 1 }), false)
    })

    it("mapKeys", () => {
      deepStrictEqual(pipe({ a: 1, b: 2, [symA]: null }, Record.mapKeys((key) => key.toUpperCase())), {
        A: 1,
        B: 2
      })
    })

    it("mapEntries", () => {
      deepStrictEqual(pipe(stringRecord, Record.mapEntries((a, key) => [key.toUpperCase(), a + 1])), { A: 2 })
    })

    describe("findFirst", () => {
      it("refinement/predicate", () => {
        const record = {
          a: 1,
          b: 2,
          c: 1
        }
        deepStrictEqual(
          pipe(record, Record.findFirst((v) => v < 2)),
          Option.some(["a", 1])
        )
        deepStrictEqual(
          pipe(record, Record.findFirst((v, k) => v < 2 && k !== "a")),
          Option.some(["c", 1])
        )
        assertNone(
          pipe(record, Record.findFirst((v) => v > 2))
        )
        deepStrictEqual(
          Record.findFirst(record, (v) => v < 2),
          Option.some(["a", 1])
        )
      })
    })
  })
})
