import { Equivalence, Number, pipe, Schema, String, String as Str, Struct, UndefinedOr } from "effect"
import { TestSchema } from "effect/testing"
import { describe, it } from "vitest"
import { assertFalse, assertTrue, deepStrictEqual, strictEqual } from "./utils/assert.ts"

const equals = TestSchema.Asserts.ast.fields.equals

describe("Struct", () => {
  it("get", () => {
    strictEqual(pipe({ a: "a", b: 1 }, Struct.get("a")), "a")
    strictEqual(pipe({ a: "a", b: 1 }, Struct.get("b")), 1)

    strictEqual(Struct.get({ a: "a", b: 1 }, "a"), "a")
    strictEqual(Struct.get({ a: "a", b: 1 }, "b"), 1)
  })

  it("keys", () => {
    const aSym = Symbol.for("a")
    deepStrictEqual(pipe({ a: 1, b: 2, [aSym]: 3 }, Struct.keys), ["a", "b"])
    deepStrictEqual(Struct.keys({ a: 1, b: 2, [aSym]: 3 }), ["a", "b"])
  })

  describe("pick", () => {
    it("defined properties", () => {
      const s: { a: string; b: number; c: boolean } = { a: "a", b: 1, c: true }
      deepStrictEqual(pipe(s, Struct.pick(["a", "b"])), { a: "a", b: 1 })
      deepStrictEqual(Struct.pick(s, ["a", "b"]), { a: "a", b: 1 })
    })

    it("omitted properties", () => {
      const s: { a?: string; b?: number; c?: boolean } = { b: 1, c: true }
      deepStrictEqual(pipe(s, Struct.pick(["a", "b"])), { b: 1 })
      deepStrictEqual(Struct.pick(s, ["a", "b"]), { b: 1 })
    })
  })

  describe("omit", () => {
    it("defined properties", () => {
      const s: { a: string; b: number; c: boolean } = { a: "a", b: 1, c: true }
      deepStrictEqual(pipe(s, Struct.omit(["c"])), { a: "a", b: 1 })
      deepStrictEqual(Struct.omit(s, ["c"]), { a: "a", b: 1 })
    })

    it("omitted properties", () => {
      const s: { a?: string; b?: number; c?: boolean } = { b: 1, c: true }
      deepStrictEqual(pipe(s, Struct.omit(["c"])), { b: 1 })
      deepStrictEqual(Struct.omit(s, ["c"]), { b: 1 })
    })

    it("ignores non-enumerable properties", () => {
      const hidden = Symbol.for("hidden")
      const s = { a: "a" }
      Object.defineProperty(s, "b", { value: 1, enumerable: false })
      Object.defineProperty(s, hidden, { value: true, enumerable: false })

      deepStrictEqual(Struct.omit(s, []), { a: "a" })
    })
  })

  describe("evolve", () => {
    it("partial required fields", () => {
      const s = { a: "a", b: 1 }
      deepStrictEqual(pipe(s, Struct.evolve({ a: (s) => s.length })), { a: 1, b: 1 })
      deepStrictEqual(Struct.evolve(s, { a: (s) => s.length }), { a: 1, b: 1 })
    })

    it("all required fields", () => {
      const s = { a: "a", b: 1 }
      deepStrictEqual(pipe(s, Struct.evolve({ a: (s) => s.length, b: (b) => b > 0 })), { a: 1, b: true })
      deepStrictEqual(Struct.evolve(s, { a: (s) => s.length, b: (b) => b > 0 }), { a: 1, b: true })
    })
  })

  it("evolveKeys", () => {
    deepStrictEqual(pipe({ a: "a", b: 2 }, Struct.evolveKeys({ a: (k) => Str.toUpperCase(k) })), { A: "a", b: 2 })
    deepStrictEqual(Struct.evolveKeys({ a: "a", b: 2 }, { a: (k) => Str.toUpperCase(k) }), { A: "a", b: 2 })
  })

  it("renameKeys", () => {
    deepStrictEqual(pipe({ a: "a", b: 1, c: true }, Struct.renameKeys({ a: "A", b: "B" })), { A: "a", B: 1, c: true })
    deepStrictEqual(Struct.renameKeys({ a: "a", b: 1, c: true }, { a: "A", b: "B" }), { A: "a", B: 1, c: true })

    const from = Symbol.for("from")
    const to = Symbol.for("to")
    deepStrictEqual(pipe({ [from]: "a", b: 1 }, Struct.renameKeys({ [from]: to })), { [to]: "a", b: 1 })
    deepStrictEqual(Struct.renameKeys({ a: "a", b: 1 }, { a: to }), { [to]: "a", b: 1 })
  })

  it("evolveEntries transforms a selected key and value together", () => {
    deepStrictEqual(
      pipe({ a: "a", b: 2 }, Struct.evolveEntries({ a: (k, v) => [Str.toUpperCase(k), v.length] })),
      { A: 1, b: 2 }
    )
    deepStrictEqual(Struct.evolveEntries({ a: "a", b: 2 }, { a: (k, v) => [Str.toUpperCase(k), v.length] }), {
      A: 1,
      b: 2
    })
  })

  it("map", () => {
    equals(pipe({ a: Schema.String, b: Schema.Number }, Struct.map(Schema.NullOr)), {
      a: Schema.NullOr(Schema.String),
      b: Schema.NullOr(Schema.Number)
    })
    equals(Struct.map({ a: Schema.String, b: Schema.Number }, Schema.NullOr), {
      a: Schema.NullOr(Schema.String),
      b: Schema.NullOr(Schema.Number)
    })
  })

  it("mapPick", () => {
    equals(
      pipe({ a: Schema.String, b: Schema.Number }, Struct.mapPick(["a"], Schema.NullOr)),
      {
        a: Schema.NullOr(Schema.String),
        b: Schema.Number
      }
    )
    equals(
      Struct.mapPick({ a: Schema.String, b: Schema.Number }, ["a"], Schema.NullOr),
      {
        a: Schema.NullOr(Schema.String),
        b: Schema.Number
      }
    )
  })

  it("mapOmit", () => {
    equals(
      pipe({ a: Schema.String, b: Schema.Number }, Struct.mapOmit(["b"], Schema.NullOr)),
      {
        a: Schema.NullOr(Schema.String),
        b: Schema.Number
      }
    )
    equals(
      Struct.mapOmit({ a: Schema.String, b: Schema.Number }, ["b"], Schema.NullOr),
      {
        a: Schema.NullOr(Schema.String),
        b: Schema.Number
      }
    )
  })

  it("makeEquivalence compares structs by configured fields", () => {
    const PersonEquivalence = Struct.makeEquivalence({
      a: Equivalence.strictEqual<string>(),
      b: Equivalence.strictEqual<number>()
    })

    assertTrue(PersonEquivalence({ a: "a", b: 1 }, { a: "a", b: 1 }))
    assertFalse(PersonEquivalence({ a: "a", b: 1 }, { a: "a", b: 2 }))
  })

  describe("makeCombiner", () => {
    it("default omitKeyWhen (never omit)", () => {
      const C = Struct.makeCombiner({
        n: Number.ReducerSum,
        s: String.ReducerConcat
      })

      deepStrictEqual(C.combine({ n: 1, s: "a" }, { n: 2, s: "b" }), { n: 3, s: "ab" })
    })

    it("custom omitKeyWhen", () => {
      const C = Struct.makeCombiner<{ n?: number | undefined; s?: string | undefined }>(
        {
          n: UndefinedOr.makeReducer(Number.ReducerSum),
          s: UndefinedOr.makeReducer(String.ReducerConcat)
        },
        { omitKeyWhen: (v) => v === undefined }
      )

      // merged values equal to undefined should be omitted
      deepStrictEqual(C.combine({ n: undefined, s: "a" }, { n: undefined, s: "b" }), { s: "ab" })
      deepStrictEqual(C.combine({ s: undefined }, { s: "b" }), { s: "b" })
      deepStrictEqual(C.combine({ s: "a" }, { s: undefined }), { s: "a" })
      deepStrictEqual(C.combine({ s: "a" }, { s: "b" }), { s: "ab" })
    })
  })

  describe("Record", () => {
    it("string keys", () => {
      deepStrictEqual(Struct.Record(["a", "b"], "value"), { a: "value", b: "value" })
    })

    it("symbol keys", () => {
      const s1 = Symbol.for("s1")
      const s2 = Symbol.for("s2")
      deepStrictEqual(Struct.Record([s1, s2], 42), { [s1]: 42, [s2]: 42 })
    })

    it("mixed keys", () => {
      const sym = Symbol.for("sym")
      deepStrictEqual(Struct.Record(["a", sym], true), { a: true, [sym]: true })
    })

    it("empty keys", () => {
      deepStrictEqual(Struct.Record([], "value"), {})
    })
  })

  describe("makeReducer", () => {
    it("custom omitKeyWhen", () => {
      const R = Struct.makeReducer<{ n: number; s?: string | undefined }>(
        {
          n: Number.ReducerSum,
          s: UndefinedOr.makeReducer(String.ReducerConcat)
        },
        { omitKeyWhen: (v) => v === undefined }
      )

      deepStrictEqual(R.initialValue, { n: 0 })
    })
  })
})
