import { describe, it } from "@effect/vitest"
import {
  BigDecimal,
  Brand,
  Cause,
  Chunk,
  Context,
  DateTime,
  Deferred,
  Duration,
  Effect,
  Equal,
  Exit,
  Fiber,
  flow,
  HashMap,
  HashSet,
  Option,
  Order,
  pipe,
  Predicate,
  Redacted,
  Result,
  Schema,
  SchemaAST,
  SchemaGetter,
  SchemaIssue,
  SchemaParser,
  SchemaTransformation,
  String as Str,
  Struct,
  Tuple
} from "effect"
import { TestSchema } from "effect/testing"
import { produce } from "immer"
import { deepStrictEqual, fail, ok, strictEqual } from "node:assert"
import { assertFalse, assertInclude, assertTrue, throws } from "../utils/assert.ts"

const verifyGeneration = true

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

const SnakeToCamel = Schema.String.pipe(
  Schema.decode(
    SchemaTransformation.snakeToCamel()
  )
)

describe("Schema", () => {
  it("isSchema", () => {
    class A extends Schema.Class<A>("A")(Schema.Struct({
      a: Schema.String
    })) {}
    class B extends Schema.Opaque<B>()(Schema.Struct({ a: Schema.String })) {}
    assertTrue(Schema.isSchema(Schema.String))
    assertTrue(Schema.isSchema(A))
    assertTrue(Schema.isSchema(B))
    assertFalse(Schema.isSchema({}))
  })

  it("toString", () => {
    const schema = Schema.String
    const result = Schema.decodeUnknownExit(schema)(null)
    assertTrue(Exit.isFailure(result))
    strictEqual(String(result.cause.reasons[0]), "Fail(SchemaError(Expected string, got null))")
  })

  describe("SchemaError", () => {
    it("extends Error and exposes the issue", () => {
      const result = SchemaParser.decodeUnknownResult(Schema.String)(null)
      assertTrue(Result.isFailure(result))
      const error = new Schema.SchemaError(result.failure)

      assertTrue(error instanceof Error)
      assertTrue(Schema.isSchemaError(error))
      strictEqual(error._tag, "SchemaError")
      strictEqual(error.name, "SchemaError")
      strictEqual(error.issue, result.failure)
      strictEqual(error.message, "Expected string, got null")
      strictEqual(String(error), "SchemaError(Expected string, got null)")
    })
  })

  describe("parseOptions annotation", () => {
    it("Number", async () => {
      const schema = Schema.Number.check(Schema.isGreaterThan(0), Schema.isInt()).annotate({
        parseOptions: { errors: "all" }
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.fail(
        -1.2,
        `Expected a value greater than 0, got -1.2
Expected an integer, got -1.2`
      )
    })

    it("Struct", async () => {
      const schema = Schema.Struct({
        a: Schema.String,
        b: Schema.Struct({
          c: Schema.String,
          d: Schema.String
        }).annotate({ parseOptions: { errors: "first" } })
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding({ parseOptions: { errors: "all" } })
      await decoding.fail(
        { a: "a", b: {} },
        `Missing key
  at ["b"]["c"]`
      )
    })

    it("should not read parseOptions from encodingChecks", async () => {
      const schema = Schema.Struct({
        a: Schema.String,
        b: Schema.String
      }).pipe(
        Schema.flip,
        Schema.check(Schema.isMaxProperties(1)),
        Schema.annotate({ parseOptions: { errors: "first" } }),
        Schema.flip
      )
      assertTrue(SchemaAST.isObjects(schema.ast))
      strictEqual(schema.ast.checks, undefined)
      strictEqual(schema.ast.encodingChecks?.length, 1)
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding({ parseOptions: { errors: "all" } })
      await decoding.fail(
        {},
        `Missing key
  at ["a"]
Missing key
  at ["b"]`
      )
    })
  })

  describe("parse options", () => {
    it("decoders can receive options when they are created", () => {
      const schema = Schema.Struct({
        a: Schema.String
      })
      const decode = Schema.decodeUnknownExit(schema, { onExcessProperty: "error" })

      const failure = decode({ a: "a", b: "b" })
      assertTrue(Exit.isFailure(failure))

      const success = decode({ a: "a", b: "b" }, { onExcessProperty: "preserve" })
      assertTrue(Exit.isSuccess(success))
      deepStrictEqual(success.value, { a: "a", b: "b" })
    })

    it("encoders can receive options when they are created", () => {
      const schema = Schema.Struct({
        a: Schema.String
      })
      const encode = Schema.encodeUnknownExit(schema, { onExcessProperty: "error" })

      const failure = encode({ a: "a", b: "b" })
      assertTrue(Exit.isFailure(failure))

      const success = encode({ a: "a", b: "b" }, { onExcessProperty: "preserve" })
      assertTrue(Exit.isSuccess(success))
      deepStrictEqual(success.value, { a: "a", b: "b" })
    })
  })

  describe("Literal", () => {
    it("should throw an error if the literal is not a finite number", () => {
      throws(
        () => Schema.Literal(Infinity),
        new Error("A numeric literal must be finite, got Infinity")
      )
      throws(
        () => Schema.Literal(-Infinity),
        new Error("A numeric literal must be finite, got -Infinity")
      )
      throws(
        () => Schema.Literal(NaN),
        new Error("A numeric literal must be finite, got NaN")
      )
    })

    it("should expose the literal", () => {
      const schema = Schema.Literal("a")
      strictEqual(schema.literal, "a")
      strictEqual(schema.annotate({}).literal, "a")
    })

    it(`"a"`, async () => {
      const schema = Schema.Literal("a")
      const asserts = new TestSchema.Asserts(schema)

      const make = asserts.make()
      await make.succeed("a")
      await make.fail(null, `Expected "a", got null`)

      const decoding = asserts.decoding()
      await decoding.succeed("a")
      await decoding.fail(1, `Expected "a", got 1`)

      const encoding = asserts.encoding()
      await encoding.succeed("a")
      await encoding.fail(1, `Expected "a", got 1`)
    })

    it(`1`, async () => {
      const schema = Schema.Literal(1)
      const asserts = new TestSchema.Asserts(schema)

      const make = asserts.make()
      await make.succeed(1)
      await make.fail(null, `Expected 1, got null`)

      const decoding = asserts.decoding()
      await decoding.succeed(1)
      await decoding.fail("1", `Expected 1, got "1"`)

      const encoding = asserts.encoding()
      await encoding.succeed(1)
      await encoding.fail("1", `Expected 1, got "1"`)
    })

    it("transform", async () => {
      const schema = Schema.Literal(0).transform("a")
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(0, "a")
      await decoding.fail(1, `Expected 0, got 1`)

      const encoding = asserts.encoding()
      await encoding.succeed("a", 0)
      await encoding.fail("b", `Expected "a", got "b"`)
    })
  })

  describe("Literals", () => {
    it("red, green, blue", async () => {
      const schema = Schema.Literals(["red", "green", "blue"])

      deepStrictEqual(schema.literals, ["red", "green", "blue"])

      const asserts = new TestSchema.Asserts(schema)
      const make = asserts.make()
      await make.succeed("red")
      await make.succeed("green")
      await make.succeed("blue")
      await make.fail("yellow", `Expected "red" | "green" | "blue", got "yellow"`)
    })

    it("transform", async () => {
      const schema = Schema.Literals([0, 1]).transform(["a", "b"])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(0, "a")
      await decoding.succeed(1, "b")
      await decoding.fail(2, `Expected 0 | 1, got 2`)

      const encoding = asserts.encoding()
      await encoding.succeed("a", 0)
      await encoding.succeed("b", 1)
      await encoding.fail("c", `Expected "a" | "b", got "c"`)
    })

    it("pick", () => {
      const schema = Schema.Literals(["a", "b", "c"]).pick(["a", "b"])

      deepStrictEqual(schema.literals, ["a", "b"])
    })
  })

  it("Never", async () => {
    const schema = Schema.Never
    const asserts = new TestSchema.Asserts(schema)

    const make = asserts.make()
    await make.fail(null as never, `Expected never, got null`)

    const decoding = asserts.decoding()
    await decoding.fail("a", `Expected never, got "a"`)

    const encoding = asserts.encoding()
    await encoding.fail("a", `Expected never, got "a"`)
  })

  it("Any", async () => {
    const schema = Schema.Any
    const asserts = new TestSchema.Asserts(schema)

    const make = asserts.make()
    await make.succeed("a")

    const decoding = asserts.decoding()
    await decoding.succeed("a")
  })

  it("Unknown", async () => {
    const schema = Schema.Unknown
    const asserts = new TestSchema.Asserts(schema)

    const make = asserts.make()
    await make.succeed("a")

    const decoding = asserts.decoding()
    await decoding.succeed("a")
  })

  it("Null", async () => {
    const schema = Schema.Null
    const asserts = new TestSchema.Asserts(schema)

    const make = asserts.make()
    await make.succeed(null)
    await make.fail(undefined, `Expected null, got undefined`)
  })

  it("Undefined", async () => {
    const schema = Schema.Undefined
    const asserts = new TestSchema.Asserts(schema)

    const make = asserts.make()
    await make.succeed(undefined)
    await make.fail(null, `Expected undefined, got null`)
  })

  it("String", async () => {
    const schema = Schema.String
    const asserts = new TestSchema.Asserts(schema)

    const make = asserts.make()
    await make.succeed("a")
    await make.fail(null, `Expected string, got null`)

    const decoding = asserts.decoding()
    await decoding.succeed("a")
    await decoding.fail(1, `Expected string, got 1`)

    const encoding = asserts.encoding()
    await encoding.succeed("a")
    await encoding.fail(1, `Expected string, got 1`)
  })

  it("Number", async () => {
    const schema = Schema.Number
    const asserts = new TestSchema.Asserts(schema)

    const make = asserts.make()
    await make.succeed(1)
    await make.fail(null, `Expected number, got null`)

    const decoding = asserts.decoding()
    await decoding.succeed(1)
    await decoding.fail("a", `Expected number, got "a"`)

    const encoding = asserts.encoding()
    await encoding.succeed(1)
    await encoding.fail("a", `Expected number, got "a"`)
  })

  it("Boolean", async () => {
    const schema = Schema.Boolean
    const asserts = new TestSchema.Asserts(schema)

    const make = asserts.make()
    await make.succeed(true)
    await make.succeed(false)
    await make.fail(null, `Expected boolean, got null`)

    const decoding = asserts.decoding()
    await decoding.succeed(true)
    await decoding.succeed(false)
    await decoding.fail("a", `Expected boolean, got "a"`)

    const encoding = asserts.encoding()
    await encoding.succeed(true)
    await encoding.succeed(false)
    await encoding.fail("a", `Expected boolean, got "a"`)
  })

  it("Symbol", async () => {
    const schema = Schema.Symbol
    const asserts = new TestSchema.Asserts(schema)

    const make = asserts.make()
    await make.succeed(Symbol("a"))
    await make.fail(null, `Expected symbol, got null`)

    const decoding = asserts.decoding()
    await decoding.succeed(Symbol("a"))
    await decoding.fail("a", `Expected symbol, got "a"`)

    const encoding = asserts.encoding()
    await encoding.succeed(Symbol("a"))
    await encoding.fail("a", `Expected symbol, got "a"`)
  })

  it("UniqueSymbol", async () => {
    const a = Symbol("a")
    const schema = Schema.UniqueSymbol(a)
    const asserts = new TestSchema.Asserts(schema)

    const make = asserts.make()
    await make.succeed(a)
    await make.fail(Symbol("b"), `Expected Symbol(a), got Symbol(b)`)

    const decoding = asserts.decoding()
    await decoding.succeed(a)
    await decoding.fail(Symbol("b"), `Expected Symbol(a), got Symbol(b)`)
  })

  it("BigInt", async () => {
    const schema = Schema.BigInt
    const asserts = new TestSchema.Asserts(schema)

    const make = asserts.make()
    await make.succeed(1n)
    await make.fail(null, `Expected bigint, got null`)

    const decoding = asserts.decoding()
    await decoding.succeed(1n)
    await decoding.fail("1", `Expected bigint, got "1"`)

    const encoding = asserts.encoding()
    await encoding.succeed(1n)
    await encoding.fail("1", `Expected bigint, got "1"`)
  })

  it("Void", async () => {
    const schema = Schema.Void
    const asserts = new TestSchema.Asserts(schema)

    // The public make input stays typed as void; these callbacks exercise runtime parser behavior.
    let fn: () => void

    const make = asserts.make()
    await make.succeed()
    await make.succeed(undefined)
    fn = () => undefined
    await make.succeed(fn())
    fn = () => null
    await make.succeed(fn(), undefined)
    fn = () => "a"
    await make.succeed(fn(), undefined)

    const decoding = asserts.decoding()
    await decoding.succeed(undefined)
    await decoding.succeed(null, undefined)
    await decoding.succeed("a", undefined)

    const encoding = asserts.encoding()
    await encoding.succeed(undefined)
    await encoding.succeed(null, undefined)
    await encoding.succeed("1", undefined)
  })

  it("ObjectKeyword", async () => {
    const schema = Schema.ObjectKeyword
    const asserts = new TestSchema.Asserts(schema)

    const make = asserts.make()
    await make.succeed({})
    await make.succeed([])
    await make.fail(null, `Expected object | array | function, got null`)

    const decoding = asserts.decoding()
    await decoding.succeed({})
    await decoding.succeed([])
    await decoding.fail("1", `Expected object | array | function, got "1"`)

    const encoding = asserts.encoding()
    await encoding.succeed({})
    await encoding.succeed([])
    await encoding.fail("1", `Expected object | array | function, got "1"`)
  })

  it("optionalKey", () => {
    const schema = Schema.optionalKey(Schema.String)
    strictEqual(schema.ast.context?.isOptional, true)
  })

  it("optionalKey & mutableKey", () => {
    const schema = Schema.String.pipe(Schema.optionalKey, Schema.mutableKey)
    strictEqual(schema.ast.context?.isOptional, true)
    strictEqual(schema.ast.context?.isMutable, true)
  })

  it("optional", () => {
    const schema = Schema.optionalKey(Schema.String)
    strictEqual(schema.ast.context?.isOptional, true)
  })

  it("mutableKey", () => {
    const schema = Schema.mutableKey(Schema.String)
    strictEqual(schema.ast.context?.isMutable, true)
  })

  it("mutableKey & optionalKey", () => {
    const schema = Schema.String.pipe(Schema.mutableKey, Schema.optionalKey)
    strictEqual(schema.ast.context?.isOptional, true)
    strictEqual(schema.ast.context?.isMutable, true)
  })

  it("readonlyKey", () => {
    const schema = Schema.readonlyKey(Schema.mutableKey(Schema.String))
    strictEqual(schema.ast.context?.isMutable, undefined)
  })

  describe("Struct", () => {
    it("should throw an error if there are duplicate property signatures", () => {
      throws(
        () =>
          new SchemaAST.Objects(
            [
              new SchemaAST.PropertySignature("a", Schema.String.ast),
              new SchemaAST.PropertySignature("b", Schema.String.ast),
              new SchemaAST.PropertySignature("c", Schema.String.ast),
              new SchemaAST.PropertySignature("a", Schema.String.ast),
              new SchemaAST.PropertySignature("c", Schema.String.ast)
            ],
            []
          ),
        new Error(`Duplicate identifiers: ["a","c"]. ts(2300)`)
      )
    })

    describe("propertyOrder", () => {
      it("all required fields", () => {
        const schema = Schema.Struct({
          a: Schema.String,
          b: Schema.String
        })

        const input = { c: "c", b: "b", a: "a", d: "d" }
        const output = Schema.decodeUnknownSync(schema)(input, {
          propertyOrder: "original",
          onExcessProperty: "preserve"
        })
        deepStrictEqual(Object.keys(output), ["c", "b", "a", "d"])
      })

      it("optional field with default", () => {
        const schema = Schema.Struct({
          a: Schema.String.pipe(Schema.encode({
            decode: SchemaGetter.withDefault(Effect.succeed("default-a")),
            encode: SchemaGetter.passthrough()
          })),
          b: Schema.String
        })

        const input = { c: "c", b: "b", d: "d" }
        const output = Schema.decodeUnknownSync(schema)(input, {
          propertyOrder: "original",
          onExcessProperty: "preserve"
        })
        deepStrictEqual(Object.keys(output), ["c", "b", "d", "a"])
      })
    })

    describe("onExcessProperty", () => {
      it("error", async () => {
        const schema = Schema.Struct({
          a: Schema.String
        })
        const asserts = new TestSchema.Asserts(schema)
        const decoding = asserts.decoding({ parseOptions: { onExcessProperty: "error" } })
        await decoding.fail(
          { a: "a", b: "b" },
          `Unexpected key with value "b"
  at ["b"]`
        )
        const sym = Symbol("sym")
        await decoding.fail(
          { a: "a", [sym]: "sym" },
          `Unexpected key with value "sym"
  at [Symbol(sym)]`
        )

        const decodingAll = asserts.decoding({ parseOptions: { onExcessProperty: "error", errors: "all" } })
        await decodingAll.fail(
          { a: "a", b: "b", c: "c" },
          `Unexpected key with value "b"
  at ["b"]
Unexpected key with value "c"
  at ["c"]`
        )
      })

      it("preserve", async () => {
        const schema = Schema.Struct({
          a: Schema.String
        })
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding({ parseOptions: { onExcessProperty: "preserve" } })
        const sym = Symbol("sym")
        await decoding.succeed(
          { a: "a", b: "b", c: "c", [sym]: "sym" }
        )
      })
    })

    it("should corectly handle __proto__", async () => {
      const schema = Schema.Struct({
        ["__proto__"]: Schema.String
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ ["__proto__"]: "a" })
      await decoding.fail(
        { __proto__: "a" },
        `Missing key
  at ["__proto__"]`
      )
    })

    it(`{ readonly "a": string }`, async () => {
      const schema = Schema.Struct({
        a: Schema.String
      })
      const asserts = new TestSchema.Asserts(schema)

      // Should be able to access the fields
      deepStrictEqual(schema.fields, { a: Schema.String })

      const make = asserts.make()
      await make.succeed({ a: "a" })
      await make.fail(null, `Expected object, got null`)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "a" })
      await decoding.fail(
        {},
        `Missing key
  at ["a"]`
      )
      await decoding.fail(
        { a: 1 },
        `Expected string, got 1
  at ["a"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed({ a: "a" })
      await encoding.fail(
        {},
        `Missing key
  at ["a"]`
      )
      await encoding.fail(
        { a: 1 },
        `Expected string, got 1
  at ["a"]`
      )
    })

    it(`{ readonly "a": <transformation> }`, async () => {
      const schema = Schema.Struct({
        a: Schema.FiniteFromString
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "1" }, { a: 1 })
      await decoding.fail(
        { a: "a" },
        `Expected a finite number, got NaN
  at ["a"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1 }, { a: "1" })
      await encoding.fail(
        { a: "a" },
        `Expected number, got "a"
  at ["a"]`
      )
    })

    it("should use the identifier annotation as the expected message", async () => {
      const schema = Schema.Struct({
        a: Schema.String
      }).annotate({ identifier: "ID" })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.fail(null, `Expected ID, got null`)
    })

    it(`Schema.optionalKey: { readonly "a"?: string }`, async () => {
      const schema = Schema.Struct({
        a: Schema.optionalKey(Schema.String)
      })
      const asserts = new TestSchema.Asserts(schema)

      const make = asserts.make()
      await make.succeed({ a: "a" })
      await make.succeed({})

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "a" })
      await decoding.succeed({})
      await decoding.fail(
        { a: 1 },
        `Expected string, got 1
  at ["a"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed({ a: "a" })
      await encoding.succeed({})
      await encoding.fail(
        { a: 1 },
        `Expected string, got 1
  at ["a"]`
      )
    })

    it(`Schema.optional: { readonly "a"?: string | undefined }`, async () => {
      const schema = Schema.Struct({
        a: Schema.optional(Schema.String)
      })
      const asserts = new TestSchema.Asserts(schema)

      const make = asserts.make()
      await make.succeed({ a: "a" })
      await make.succeed({ a: undefined })
      await make.succeed({})

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "a" })
      await decoding.succeed({ a: undefined })
      await decoding.succeed({})
      await decoding.fail(
        { a: 1 },
        `Expected string | undefined, got 1
  at ["a"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed({ a: "a" })
      await encoding.succeed({ a: undefined })
      await encoding.succeed({})
      await encoding.fail(
        { a: 1 },
        `Expected string | undefined, got 1
  at ["a"]`
      )
    })

    it(`{ readonly "a"?: <transformation> }`, async () => {
      const schema = Schema.Struct({
        a: Schema.optionalKey(Schema.FiniteFromString)
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "1" }, { a: 1 })
      await decoding.succeed({})
      await decoding.fail(
        { a: undefined },
        `Expected string, got undefined
  at ["a"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1 }, { a: "1" })
      await encoding.succeed({})
    })

    describe("ParseOptions", () => {
      it(`{ errors: "all" }`, async () => {
        const schema = Schema.Struct({
          a: Schema.String,
          b: Schema.Number
        })
        const asserts = new TestSchema.Asserts(schema)

        const make = asserts.make({ parseOptions: { errors: "all" } })
        await make.fail(
          {},
          `Missing key
  at ["a"]
Missing key
  at ["b"]`
        )

        const decoding = asserts.decoding({ parseOptions: { errors: "all" } })
        await decoding.fail(
          {},
          `Missing key
  at ["a"]
Missing key
  at ["b"]`
        )

        const encoding = asserts.encoding({ parseOptions: { errors: "all" } })
        await encoding.fail(
          {},
          `Missing key
  at ["a"]
Missing key
  at ["b"]`
        )
      })
    })

    describe("assign", () => {
      it("Struct", async () => {
        const from = Schema.Struct({
          a: Schema.String
        })
        const schema = from.mapFields(Struct.assign({ b: Schema.String }))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed({ a: "a", b: "b" })
        await decoding.fail(
          { b: "b" },
          `Missing key
  at ["a"]`
        )
        await decoding.fail(
          { a: "a" },
          `Missing key
  at ["b"]`
        )
      })

      it("overlapping fields", async () => {
        const from = Schema.Struct({
          a: Schema.String,
          b: Schema.String
        })
        const schema = from.mapFields(Struct.assign({ b: Schema.Number, c: Schema.Number }))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed({ a: "a", b: 1, c: 2 })
        await decoding.fail(
          { a: "a", b: "b" },
          `Expected number, got "b"
  at ["b"]`
        )
      })

      it("Struct & check & unsafePreserveChecks: true", async () => {
        const from = Schema.Struct({
          a: Schema.String,
          b: Schema.String
        }).check(
          Schema.makeFilter(({ a, b }) => a === b, { expected: "a === b" })
        )
        const schema = from.mapFields(Struct.assign({ c: Schema.String }), { unsafePreserveChecks: true })
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed({ a: "a", b: "a", c: "c" })
        await decoding.fail(
          { a: "", b: "b", c: "c" },
          `Expected a === b, got {"a":"","b":"b","c":"c"}`
        )
      })
    })

    describe("pick", () => {
      it("Struct", async () => {
        const schema = Schema.Struct({
          a: Schema.String,
          b: Schema.String
        }).mapFields(Struct.pick(["a"]))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed({ a: "a" })
      })
    })

    describe("omit", () => {
      it("Struct", async () => {
        const schema = Schema.Struct({
          a: Schema.String,
          b: Schema.String
        }).mapFields(Struct.omit(["b"]))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed({ a: "a" })
      })
    })
  })

  describe("Tuple", () => {
    it("A required element cannot follow an optional element", () => {
      throws(
        () => Schema.Tuple([Schema.optionalKey(Schema.String), Schema.String]),
        new Error("A required element cannot follow an optional element. ts(1257)")
      )
      throws(
        () => Schema.Tuple([Schema.optional(Schema.String), Schema.String]),
        new Error("A required element cannot follow an optional element. ts(1257)")
      )
    })

    it("should fail on unexpected indexes", async () => {
      const schema = Schema.Tuple([Schema.String])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.fail(
        ["a", "b"],
        `Unexpected key with value "b"
  at [1]`
      )
      const decodingAll = asserts.decoding({ parseOptions: { errors: "all" } })
      await decodingAll.fail(
        ["a", "b", "c"],
        `Unexpected key with value "b"
  at [1]
Unexpected key with value "c"
  at [2]`
      )
    })

    it(`readonly [string]`, async () => {
      const schema = Schema.Tuple([Schema.NonEmptyString])
      const asserts = new TestSchema.Asserts(schema)

      // should be able to access the elements
      deepStrictEqual(schema.elements, [Schema.NonEmptyString])

      const make = asserts.make()
      await make.succeed(["a"])
      await make.fail(
        [""],
        `Expected a value with a length of at least 1, got ""
  at [0]`
      )

      const decoding = asserts.decoding()
      await decoding.succeed(["a"])
      await decoding.fail(null, `Expected array, got null`)
      await decoding.fail(
        [],
        `Missing key
  at [0]`
      )
      await decoding.fail(
        [1],
        `Expected string, got 1
  at [0]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed(["a"])
      await encoding.fail(
        [],
        `Missing key
  at [0]`
      )
      await encoding.fail(
        [1],
        `Expected string, got 1
  at [0]`
      )
    })

    it(`readonly [string?]`, async () => {
      const schema = Schema.Tuple([Schema.String.pipe(Schema.optionalKey)])
      const asserts = new TestSchema.Asserts(schema)

      const make = asserts.make()
      await make.succeed(["a"])
      await make.succeed([])

      const decoding = asserts.decoding()
      await decoding.succeed(["a"])
      await decoding.succeed([])

      const encoding = asserts.encoding()
      await encoding.succeed(["a"])
      await encoding.succeed([])
    })
  })

  describe("Array", () => {
    it("should expose the element schema via .value", () => {
      const schema = Schema.Array(Schema.String)
      strictEqual(schema.value, Schema.String)
      strictEqual(schema.annotate({}).value, Schema.String)
    })

    it("readonly string[]", async () => {
      const schema = Schema.Array(Schema.String)
      const asserts = new TestSchema.Asserts(schema)

      const make = asserts.make()
      await make.succeed(["a", "b"])

      const decoding = asserts.decoding()
      await decoding.succeed(["a", "b"])
      await decoding.fail(
        ["a", 1],
        `Expected string, got 1
  at [1]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed(["a", "b"])
      await encoding.fail(
        ["a", 1],
        `Expected string, got 1
  at [1]`
      )
    })
  })

  it("ArrayEnsure", async () => {
    const schema = Schema.ArrayEnsure(Schema.FiniteFromString)
    const asserts = new TestSchema.Asserts(schema)

    const decoding = asserts.decoding()
    await decoding.succeed("1", [1])
    await decoding.succeed(["1", "2"], [1, 2])
    await decoding.succeed([], [])
    await decoding.fail(null, `Expected string | array, got null`)
    await decoding.fail("a", `Expected a finite number, got NaN`)
    await decoding.fail(
      ["a"],
      `Expected a finite number, got NaN
  at [0]`
    )

    const encoding = asserts.encoding()
    await encoding.succeed([], [])
    await encoding.succeed([1], "1")
    await encoding.succeed([1, 2], ["1", "2"])
  })

  describe("NonEmptyArray", () => {
    it("should expose the element schema via .value", () => {
      const schema = Schema.NonEmptyArray(Schema.String)
      strictEqual(schema.value, Schema.String)
    })

    it("readonly string[]", async () => {
      const schema = Schema.NonEmptyArray(Schema.String)
      const asserts = new TestSchema.Asserts(schema)

      const make = asserts.make()
      await make.succeed(["a"])
      await make.succeed(["a", "b"])

      const decoding = asserts.decoding()
      await decoding.succeed(["a"])
      await decoding.succeed(["a", "b"])
      await decoding.fail(
        [],
        `Missing key
  at [0]`
      )
      await decoding.fail(
        ["a", 1],
        `Expected string, got 1
  at [1]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed(["a"])
      await encoding.succeed(["a", "b"])
      await encoding.fail(
        [],
        `Missing key
  at [0]`
      )
      await encoding.fail(
        ["a", 1],
        `Expected string, got 1
  at [1]`
      )
    })
  })

  it("Trimmed", async () => {
    const schema = Schema.Trimmed
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed("a")
    await decoding.fail(
      " a ",
      `Expected a string with no leading or trailing whitespace, got " a "`
    )
  })

  describe("Checks", () => {
    describe("check", () => {
      it("single check", async () => {
        const schema = Schema.String.check(Schema.isMinLength(3))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed("abc")
        await decoding.fail(
          "ab",
          `Expected a value with a length of at least 3, got "ab"`
        )
      })

      it("multiple checks", async () => {
        const schema = Schema.String.check(
          Schema.isMinLength(3),
          Schema.isIncludes("c")
        )
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed("abc")
        await decoding.fail(
          "ab",
          `Expected a value with a length of at least 3, got "ab"`
        )
        const decodingAll = asserts.decoding({ parseOptions: { errors: "all" } })
        await decodingAll.fail(
          "ab",
          `Expected a value with a length of at least 3, got "ab"
Expected a string including "c", got "ab"`
        )
      })

      it("aborting checks", async () => {
        const schema = Schema.String.check(
          Schema.isMinLength(2).abort(),
          Schema.isIncludes("b")
        )
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.fail(
          "a",
          `Expected a value with a length of at least 2, got "a"`
        )
      })

      it("makeFilterGroup", async () => {
        const usernameGroup = Schema.makeFilterGroup(
          [
            Schema.isMinLength(3),
            Schema.isPattern(/^[a-zA-Z0-9]+$/, {
              title: "alphanumeric",
              description: "must contain only letters and numbers"
            }),
            Schema.isTrimmed()
          ],
          {
            title: "username",
            description: "a valid username"
          }
        )

        const Username = Schema.String.check(usernameGroup)
        const asserts = new TestSchema.Asserts(Username)

        const decoding = asserts.decoding()
        await decoding.succeed("abc")
        await decoding.fail(
          "",
          `Expected a value with a length of at least 3, got ""`
        )
      })

      it("object-level checks are validated against the decoded value when encoding", async () => {
        const schema = Schema.Struct({ a: Schema.FiniteFromString }).check(
          Schema.makeFilter((o) => typeof o.a === "number", { expected: "a is a number" })
        )
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed({ a: "1" }, { a: 1 })

        const encoding = asserts.encoding()
        await encoding.succeed({ a: 1 }, { a: "1" })
      })

      it("suspended checks are not supported", () => {
        throws(
          () => {
            Schema.suspend(() => Schema.Struct({ a: Schema.FiniteFromString })).check(
              Schema.makeFilter((o) => typeof o.a === "number", { expected: "a is a number" })
            )
          },
          "Cannot add checks to Suspend"
        )
      })
    })

    it("refine", async () => {
      const schema = Schema.Option(Schema.String).pipe(
        Schema.refine(Option.isSome, { expected: "isSome" }),
        Schema.check(
          Schema.makeFilter(({ value }) => value.length > 0, { expected: "length > 0" })
        )
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(Option.some("a"))
      await decoding.fail(
        Option.some(""),
        `Expected length > 0, got some("")`
      )
      await decoding.fail(
        Option.none(),
        `Expected isSome, got none()`
      )
    })

    describe("String checks", () => {
      it("isPattern", async () => {
        const schema = Schema.String.check(Schema.isPattern(/^a/))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed("a")
        await decoding.fail(
          "b",
          `Expected a string matching the RegExp ^a, got "b"`
        )

        const encoding = asserts.encoding()
        await encoding.succeed("a")
        await encoding.fail(
          "b",
          `Expected a string matching the RegExp ^a, got "b"`
        )
      })

      it("isStartsWith", async () => {
        const schema = Schema.String.check(Schema.isStartsWith("a"))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed("a")
        await decoding.fail(
          "b",
          `Expected a string starting with "a", got "b"`
        )

        const encoding = asserts.encoding()
        await encoding.succeed("a")
        await encoding.fail(
          "b",
          `Expected a string starting with "a", got "b"`
        )
      })

      it("isEndsWith", async () => {
        const schema = Schema.String.check(Schema.isEndsWith("a"))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed("a")
        await decoding.fail(
          "b",
          `Expected a string ending with "a", got "b"`
        )

        const encoding = asserts.encoding()
        await encoding.succeed("a")
        await encoding.fail(
          "b",
          `Expected a string ending with "a", got "b"`
        )
      })

      it("isLowercased", async () => {
        const schema = Schema.String.check(Schema.isLowercased())
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed("a")
        await decoding.fail(
          "A",
          `Expected a string with all characters in lowercase, got "A"`
        )

        const encoding = asserts.encoding()
        await encoding.succeed("a")
        await encoding.fail(
          "A",
          `Expected a string with all characters in lowercase, got "A"`
        )
      })

      it("isUppercased", async () => {
        const schema = Schema.String.check(Schema.isUppercased())
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed("A")
        await decoding.fail(
          "a",
          `Expected a string with all characters in uppercase, got "a"`
        )

        const encoding = asserts.encoding()
        await encoding.succeed("A")
        await encoding.fail(
          "a",
          `Expected a string with all characters in uppercase, got "a"`
        )
      })

      it("isCapitalized", async () => {
        const schema = Schema.String.check(Schema.isCapitalized())
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed("Abc")
        await decoding.fail(
          "abc",
          `Expected a string with the first character in uppercase, got "abc"`
        )

        const encoding = asserts.encoding()
        await encoding.succeed("Abc")
        await encoding.fail(
          "abc",
          `Expected a string with the first character in uppercase, got "abc"`
        )
      })

      it("isUncapitalized", async () => {
        const schema = Schema.String.check(Schema.isUncapitalized())
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed("aBC")
        await decoding.fail(
          "ABC",
          `Expected a string with the first character in lowercase, got "ABC"`
        )

        const encoding = asserts.encoding()
        await encoding.succeed("aBC")
        await encoding.fail(
          "ABC",
          `Expected a string with the first character in lowercase, got "ABC"`
        )
      })

      it("isMinLength", async () => {
        const schema = Schema.String.check(Schema.isMinLength(1))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed("a")
        await decoding.fail(
          "",
          `Expected a value with a length of at least 1, got ""`
        )

        const encoding = asserts.encoding()
        await encoding.succeed("a")
        await encoding.fail(
          "",
          `Expected a value with a length of at least 1, got ""`
        )
      })
    })

    describe("Number checks", () => {
      it("isGreaterThan", async () => {
        const schema = Schema.Number.check(Schema.isGreaterThan(1))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed(2)
        await decoding.fail(
          1,
          `Expected a value greater than 1, got 1`
        )

        const encoding = asserts.encoding()
        await encoding.succeed(2)
        await encoding.fail(
          1,
          `Expected a value greater than 1, got 1`
        )
      })

      it("isGreaterThanOrEqualTo", async () => {
        const schema = Schema.Number.check(Schema.isGreaterThanOrEqualTo(1))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed(1)
        await decoding.fail(
          0,
          `Expected a value greater than or equal to 1, got 0`
        )
      })

      it("isLessThan", async () => {
        const schema = Schema.Number.check(Schema.isLessThan(1))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed(0)
        await decoding.fail(
          1,
          `Expected a value less than 1, got 1`
        )
      })

      it("isLessThanOrEqualTo", async () => {
        const schema = Schema.Number.check(Schema.isLessThanOrEqualTo(1))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed(1)
        await decoding.fail(
          2,
          `Expected a value less than or equal to 1, got 2`
        )
      })

      it("isMultipleOf", async () => {
        const schema = Schema.Number.check(Schema.isMultipleOf(2))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed(4)
        await decoding.fail(
          3,
          `Expected a value that is a multiple of 2, got 3`
        )
      })

      it("isMultipleOf rejects nonzero subnormal remainders", () => {
        const is = Schema.is(Schema.Number.check(Schema.isMultipleOf(Number("1e-323"))))

        assertFalse(is(Number("1.042e-321")))
      })

      describe("isBetween", () => {
        it("included & included", async () => {
          const schema = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 3 }))
          const asserts = new TestSchema.Asserts(schema)

          const decoding = asserts.decoding()
          await decoding.succeed(1)
          await decoding.succeed(3)
          await decoding.fail(
            0,
            `Expected a value between 1 and 3, got 0`
          )
          await decoding.fail(
            4,
            `Expected a value between 1 and 3, got 4`
          )

          const encoding = asserts.encoding()
          await encoding.succeed(1)
          await encoding.succeed(3)
          await encoding.fail(
            0,
            `Expected a value between 1 and 3, got 0`
          )
        })

        it("included & excluded", async () => {
          const schema = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 3, exclusiveMaximum: true }))
          const asserts = new TestSchema.Asserts(schema)

          const decoding = asserts.decoding()
          await decoding.succeed(1)
          await decoding.fail(3, `Expected a value between 1 and 3 (excluded), got 3`)
          await decoding.fail(
            0,
            `Expected a value between 1 and 3 (excluded), got 0`
          )
          await decoding.fail(
            4,
            `Expected a value between 1 and 3 (excluded), got 4`
          )

          const encoding = asserts.encoding()
          await encoding.succeed(1)
          await encoding.fail(3, `Expected a value between 1 and 3 (excluded), got 3`)
          await encoding.fail(
            0,
            `Expected a value between 1 and 3 (excluded), got 0`
          )
        })

        it("excluded & included", async () => {
          const schema = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 3, exclusiveMinimum: true }))
          const asserts = new TestSchema.Asserts(schema)

          const decoding = asserts.decoding()
          await decoding.fail(1, `Expected a value between 1 (excluded) and 3, got 1`)
          await decoding.succeed(3)
          await decoding.fail(
            0,
            `Expected a value between 1 (excluded) and 3, got 0`
          )
          await decoding.fail(
            4,
            `Expected a value between 1 (excluded) and 3, got 4`
          )

          const encoding = asserts.encoding()
          await encoding.fail(1, `Expected a value between 1 (excluded) and 3, got 1`)
          await encoding.succeed(3)
          await encoding.fail(
            0,
            `Expected a value between 1 (excluded) and 3, got 0`
          )
        })

        it("excluded & excluded", async () => {
          const schema = Schema.Int.check(
            Schema.isBetween({ minimum: 1, maximum: 3, exclusiveMinimum: true, exclusiveMaximum: true })
          )
          const asserts = new TestSchema.Asserts(schema)

          const decoding = asserts.decoding()
          await decoding.succeed(2)
          await decoding.fail(1, `Expected a value between 1 (excluded) and 3 (excluded), got 1`)
          await decoding.fail(3, `Expected a value between 1 (excluded) and 3 (excluded), got 3`)
          await decoding.fail(
            0,
            `Expected a value between 1 (excluded) and 3 (excluded), got 0`
          )
          await decoding.fail(
            4,
            `Expected a value between 1 (excluded) and 3 (excluded), got 4`
          )

          const encoding = asserts.encoding()
          await encoding.succeed(2)
          await encoding.fail(1, `Expected a value between 1 (excluded) and 3 (excluded), got 1`)
          await encoding.fail(3, `Expected a value between 1 (excluded) and 3 (excluded), got 3`)
          await encoding.fail(
            0,
            `Expected a value between 1 (excluded) and 3 (excluded), got 0`
          )
        })
      })

      it("isInt", async () => {
        const schema = Schema.Number.check(Schema.isInt())
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed(1)
        await decoding.fail(
          1.1,
          `Expected an integer, got 1.1`
        )

        const encoding = asserts.encoding()
        await encoding.succeed(1)
        await encoding.fail(
          1.1,
          `Expected an integer, got 1.1`
        )
        await decoding.fail(
          NaN,
          `Expected an integer, got NaN`
        )
        await decoding.fail(
          Infinity,
          `Expected an integer, got Infinity`
        )
        await decoding.fail(
          -Infinity,
          `Expected an integer, got -Infinity`
        )
      })

      it("isInt32", async () => {
        const schema = Schema.Number.check(Schema.isInt32())
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed(1)
        await decoding.fail(
          1.1,
          `Expected an integer, got 1.1`
        )
        await decoding.fail(
          Number.MAX_SAFE_INTEGER + 1,
          `Expected an integer, got 9007199254740992`
        )
        await decoding.fail(
          1.1,
          `Expected an integer, got 1.1`
        )
        await decoding.fail(
          Number.MIN_SAFE_INTEGER - 1,
          `Expected an integer, got -9007199254740992`
        )
        const decodingAll = asserts.decoding({ parseOptions: { errors: "all" } })
        await decodingAll.fail(
          Number.MAX_SAFE_INTEGER + 1,
          `Expected an integer, got 9007199254740992
Expected a value between -2147483648 and 2147483647, got 9007199254740992`
        )

        const encoding = asserts.encoding()
        await encoding.succeed(1)
        await encoding.fail(
          1.1,
          `Expected an integer, got 1.1`
        )
        await encoding.fail(
          Number.MAX_SAFE_INTEGER + 1,
          `Expected an integer, got 9007199254740992`
        )
      })
    })

    describe("BigInt Checks", () => {
      const options = { order: Order.BigInt, format: (value: bigint) => `${value}n` }

      const isBetween = Schema.makeIsBetween(options)
      const isGreaterThan = Schema.makeIsGreaterThan(options)
      const isGreaterThanOrEqualTo = Schema.makeIsGreaterThanOrEqualTo(options)
      const isLessThan = Schema.makeIsLessThan(options)
      const isLessThanOrEqualTo = Schema.makeIsLessThanOrEqualTo(options)

      it("isBetween", async () => {
        const schema = Schema.BigInt.check(isBetween({ minimum: 5n, maximum: 10n }))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed(5n)
        await decoding.succeed(7n)
        await decoding.succeed(10n)
        await decoding.fail(
          4n,
          `Expected a value between 5n and 10n, got 4n`
        )
      })

      it("isGreaterThan", async () => {
        const schema = Schema.BigInt.check(isGreaterThan(5n))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed(6n)
        await decoding.fail(
          5n,
          `Expected a value greater than 5n, got 5n`
        )
      })

      it("isGreaterThanOrEqualTo", async () => {
        const schema = Schema.BigInt.check(isGreaterThanOrEqualTo(5n))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed(5n)
        await decoding.succeed(6n)
        await decoding.fail(
          4n,
          `Expected a value greater than or equal to 5n, got 4n`
        )
      })

      it("isLessThan", async () => {
        const schema = Schema.BigInt.check(isLessThan(5n))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed(4n)
        await decoding.fail(
          5n,
          `Expected a value less than 5n, got 5n`
        )
      })

      it("isLessThanOrEqualTo", async () => {
        const schema = Schema.BigInt.check(isLessThanOrEqualTo(5n))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed(5n)
        await decoding.succeed(4n)
        await decoding.fail(
          6n,
          `Expected a value less than or equal to 5n, got 6n`
        )
      })
    })

    describe("Record checks", () => {
      it("isMinProperties", async () => {
        const schema = Schema.Record(Schema.String, Schema.Finite).check(Schema.isMinProperties(1))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed({ a: 1, b: 2 })
        await decoding.fail(
          {},
          `Expected a value with at least 1 entry, got {}`
        )
      })

      it("isMaxProperties", async () => {
        const schema = Schema.Record(Schema.String, Schema.Finite).check(Schema.isMaxProperties(2))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed({ a: 1, b: 2 })
        await decoding.fail(
          { a: 1, b: 2, c: 3 },
          `Expected a value with at most 2 entries, got {"a":1,"b":2,"c":3}`
        )
        await decoding.fail(
          { a: 1, b: 2, c: 3 },
          `Expected a value with at most 2 entries, got {"a":1,"b":2,"c":3}`
        )
      })

      it("isMinProperties with symbol keys", async () => {
        const sym = Symbol("test")
        const schema = Schema.Record(Schema.Union([Schema.String, Schema.Symbol]), Schema.Finite).check(
          Schema.isMinProperties(2)
        )
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed({ a: 1, [sym]: 2 })
        await decoding.fail(
          { [sym]: 1 },
          `Expected a value with at least 2 entries, got {Symbol(test):1}`
        )
      })

      it("isMaxProperties with symbol keys", async () => {
        const sym1 = Symbol("test1")
        const sym2 = Symbol("test2")
        const sym3 = Symbol("test3")
        const schema = Schema.Record(Schema.Union([Schema.String, Schema.Symbol]), Schema.Finite).check(
          Schema.isMaxProperties(2)
        )
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed({ [sym1]: 1, [sym2]: 2 })
        await decoding.fail(
          { [sym1]: 1, [sym2]: 2, [sym3]: 3 },
          `Expected a value with at most 2 entries, got {Symbol(test1):1,Symbol(test2):2,Symbol(test3):3}`
        )
      })

      it("isPropertiesLengthBetween", async () => {
        const schema = Schema.Record(Schema.String, Schema.Number).check(Schema.isPropertiesLengthBetween(2, 2))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed({ a: 1, b: 2 })
        await decoding.succeed({ ["__proto__"]: 0, "": 0 })
        await decoding.fail(
          { a: 1 },
          `Expected a value with exactly 2 entries, got {"a":1}`
        )
        await decoding.fail(
          { a: 1, b: 2, c: 3 },
          `Expected a value with exactly 2 entries, got {"a":1,"b":2,"c":3}`
        )
      })

      it("isPropertiesLengthBetween with symbol keys", async () => {
        const sym1 = Symbol("test1")
        const sym2 = Symbol("test2")
        const schema = Schema.Record(Schema.Union([Schema.String, Schema.Symbol]), Schema.Number).check(
          Schema.isPropertiesLengthBetween(2, 2)
        )
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed({ [sym1]: 1, [sym2]: 2 })
        await decoding.succeed({ a: 1, [sym1]: 2 })
        await decoding.fail(
          { [sym1]: 1 },
          `Expected a value with exactly 2 entries, got {Symbol(test1):1}`
        )
        await decoding.fail(
          { [sym1]: 1, [sym2]: 2, a: 3 },
          `Expected a value with exactly 2 entries, got {"a":3,Symbol(test1):1,Symbol(test2):2}`
        )
      })

      it("isPropertyNames", async () => {
        const schema = Schema.Record(Schema.String, Schema.Finite).check(
          Schema.isPropertyNames(Schema.String.check(Schema.isPattern(/^[A-Z]/)))
        )
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed({ Ab: 1 })
        await decoding.fail(
          { ab: 1 },
          `Expected a string matching the RegExp ^[A-Z], got "ab"
  at ["ab"]`
        )
      })

      it("isPropertyNames with Never", async () => {
        const schema = Schema.Record(Schema.String, Schema.Finite).check(Schema.isPropertyNames(Schema.Never))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed({})
        await decoding.fail(
          { a: 1 },
          `Expected never, got "a"
  at ["a"]`
        )
      })
    })

    describe("Structural checks", () => {
      it("Array + isMinLength", async () => {
        const schema = Schema.Struct({
          tags: Schema.Array(Schema.NonEmptyString).check(Schema.isMinLength(3))
        })
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.fail(
          {},
          `Missing key
  at ["tags"]`
        )
        const decodingAll = asserts.decoding({ parseOptions: { errors: "all" } })
        await decodingAll.fail(
          { tags: ["a", ""] },
          `Expected a value with a length of at least 1, got ""
  at ["tags"][1]
Expected a value with a length of at least 3, got ["a",""]
  at ["tags"]`
        )
      })

      it("Record + isMaxProperties", async () => {
        const schema = Schema.Record(Schema.String, Schema.Finite).check(Schema.isMaxProperties(2))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.fail(
          null,
          `Expected object, got null`
        )
        const decodingAll = asserts.decoding({ parseOptions: { errors: "all" } })
        await decodingAll.fail(
          { a: 1, b: NaN, c: 3 },
          `Expected a finite number, got NaN
  at ["b"]
Expected a value with at most 2 entries, got {"a":1,"b":NaN,"c":3}`
        )
      })

      it("ReadonlyMap + isMaxSize", async () => {
        const schema = Schema.ReadonlyMap(Schema.String, Schema.Finite).check(Schema.isMaxSize(2))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.fail(
          null,
          `Expected ReadonlyMap, got null`
        )
        const decodingAll = asserts.decoding({ parseOptions: { errors: "all" } })
        await decodingAll.fail(
          new Map([["a", 1], ["b", NaN], ["c", 3]]),
          `Expected a finite number, got NaN
  at ["entries"][1][1]
Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])`
        )
      })
    })

    describe("Array checks", () => {
      it("UniqueArray", async () => {
        const schema = Schema.UniqueArray(Schema.Struct({
          a: Schema.String,
          b: Schema.String
        }))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed([{ a: "a", b: "b" }, { a: "c", b: "d" }])
        await decoding.fail(
          [{ a: "a", b: "b" }, { a: "a", b: "b" }],
          `Expected an array with unique items, got [{"a":"a","b":"b"},{"a":"a","b":"b"}]`
        )
      })
    })
  })

  it("Finite", async () => {
    const schema = Schema.Finite
    const asserts = new TestSchema.Asserts(schema)
    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }
  })

  describe("Transformations", () => {
    it("NumberFromString", async () => {
      const schema = Schema.NumberFromString
      const asserts = new TestSchema.Asserts(schema)

      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const decoding = asserts.decoding()
      await decoding.succeed("1", 1)
      await decoding.succeed("NaN", NaN)
      await decoding.succeed("Infinity", Infinity)
      await decoding.succeed("+Infinity", Infinity)
      await decoding.succeed("-Infinity", -Infinity)

      const encoding = asserts.encoding()
      await encoding.succeed(1, "1")
      await encoding.succeed(NaN, "NaN")
      await encoding.succeed(Infinity, "Infinity")
      await encoding.succeed(-Infinity, "-Infinity")
      await encoding.fail(
        "a",
        `Expected number, got "a"`
      )
    })

    it("DateFromString", async () => {
      const schema = Schema.DateFromString
      const asserts = new TestSchema.Asserts(schema)
      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const decoding = asserts.decoding()
      await decoding.succeed("2021-01-01T00:00:00.000Z", new Date("2021-01-01T00:00:00.000Z"))

      const encoding = asserts.encoding()
      await encoding.succeed(new Date("2021-01-01T00:00:00.000Z"), "2021-01-01T00:00:00.000Z")
    })

    it("DateFromMillis", async () => {
      const schema = Schema.DateFromMillis
      const asserts = new TestSchema.Asserts(schema)
      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const decoding = asserts.decoding()
      await decoding.succeed(0, new Date(0))
      assertTrue(Schema.decodeSync(schema)(NaN) instanceof Date)
      assertTrue(Schema.decodeSync(schema)(Infinity) instanceof Date)
      assertTrue(Schema.decodeSync(schema)(-Infinity) instanceof Date)
      await decoding.fail(null, `Expected number, got null`)

      const encoding = asserts.encoding()
      await encoding.succeed(new Date(0), 0)
      strictEqual(Schema.encodeSync(schema)(new Date("invalid")), NaN)
      strictEqual(Schema.encodeSync(schema)(new Date(NaN)), NaN)
      strictEqual(Schema.encodeSync(schema)(new Date(Infinity)), NaN)
      strictEqual(Schema.encodeSync(schema)(new Date(-Infinity)), NaN)
    })

    it("FiniteFromString", async () => {
      const schema = Schema.FiniteFromString
      const asserts = new TestSchema.Asserts(schema)

      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const decoding = asserts.decoding()
      await decoding.succeed("1", 1)
      await decoding.fail(
        "a",
        `Expected a finite number, got NaN`
      )
      await decoding.fail(
        "NaN",
        `Expected a finite number, got NaN`
      )
      await decoding.fail(
        "Infintiy",
        `Expected a finite number, got NaN`
      )
      await decoding.fail(
        "+Infintiy",
        `Expected a finite number, got NaN`
      )
      await decoding.fail(
        "-Infintiy",
        `Expected a finite number, got NaN`
      )

      const encoding = asserts.encoding()
      await encoding.succeed(1, "1")
      await encoding.fail(
        "a",
        `Expected number, got "a"`
      )
    })

    it("BigIntFromString", async () => {
      const schema = Schema.BigIntFromString
      const asserts = new TestSchema.Asserts(schema)
      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const decoding = asserts.decoding()
      await decoding.succeed("0", 0n)
      await decoding.fail(
        "a",
        `Expected a string representing a bigint, got "a"`
      )

      const encoding = asserts.encoding()
      await encoding.succeed(0n, "0")
    })

    it("BigDecimalFromString", async () => {
      const schema = Schema.BigDecimalFromString
      const asserts = new TestSchema.Asserts(schema)

      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const decoding = asserts.decoding()
      await decoding.succeed("0", Option.getOrThrow(BigDecimal.fromString("0")))
      await decoding.succeed("123.456", Option.getOrThrow(BigDecimal.fromString("123.456")))

      const encoding = asserts.encoding()
      await encoding.succeed(BigDecimal.make(0n, 0), "0")
      await encoding.succeed(BigDecimal.make(123456n, 3), "123.456")
      await encoding.fail(
        "a",
        `Expected BigDecimal, got "a"`
      )
    })

    it("TimeZoneNamedFromString", async () => {
      const schema = Schema.TimeZoneNamedFromString
      const asserts = new TestSchema.Asserts(schema)

      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const decoding = asserts.decoding()
      await decoding.succeed("Europe/London", DateTime.zoneMakeNamedUnsafe("Europe/London"))

      const encoding = asserts.encoding()
      await encoding.succeed(DateTime.zoneMakeNamedUnsafe("Europe/London"), "Europe/London")
      await encoding.fail(
        "a",
        `Expected DateTime.TimeZone.Named, got "a"`
      )
    })

    it("TimeZoneFromString", async () => {
      const schema = Schema.TimeZoneFromString
      const asserts = new TestSchema.Asserts(schema)

      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const decoding = asserts.decoding()
      await decoding.succeed("Europe/London", DateTime.zoneMakeNamedUnsafe("Europe/London"))
      await decoding.succeed("+03:00", DateTime.zoneMakeOffset(3 * 60 * 60 * 1000))

      const encoding = asserts.encoding()
      await encoding.succeed(DateTime.zoneMakeNamedUnsafe("Europe/London"), "Europe/London")
      await encoding.succeed(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000), "+03:00")
      await encoding.fail(
        "a",
        `Expected DateTime.TimeZone, got "a"`
      )
    })

    it("DateTimeZonedFromString", async () => {
      const schema = Schema.DateTimeZonedFromString
      const asserts = new TestSchema.Asserts(schema)

      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const zoned = DateTime.makeZonedUnsafe("2021-01-01T00:00:00.000Z", { timeZone: "Europe/London" })

      const decoding = asserts.decoding()
      await decoding.fail("invalid", `Invalid Zoned DateTime string: invalid`)

      const encoding = asserts.encoding()
      await encoding.succeed(zoned, DateTime.formatIsoZoned(zoned))
      await encoding.fail(
        "a",
        `Expected DateTime.Zoned, got "a"`
      )
    })

    it("FiniteFromString & isGreaterThan", async () => {
      const schema = Schema.FiniteFromString.check(Schema.isGreaterThan(2))
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("3", 3)
      await decoding.fail(
        "1",
        `Expected a value greater than 2, got 1`
      )

      const encoding = asserts.encoding()
      await encoding.succeed(3, "3")
      await encoding.fail(
        1,
        `Expected a value greater than 2, got 1`
      )
    })
  })

  describe("decodeTo", () => {
    it("should expose the source and the target schemas", () => {
      const schema = Schema.FiniteFromString

      strictEqual(schema.from.ast._tag, "String")
      strictEqual(schema.to, Schema.Finite)
    })

    it("required to required", async () => {
      const schema = Schema.Struct({
        a: Schema.String.pipe(
          Schema.decodeTo(
            Schema.String,
            SchemaTransformation.passthrough()
          )
        )
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "a" })
      await decoding.fail(
        {},
        `Missing key
  at ["a"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed({ a: "a" })
      await encoding.fail(
        {},
        `Missing key
  at ["a"]`
      )
    })

    it("required to optional", async () => {
      const schema = Schema.Struct({
        a: Schema.String.pipe(
          Schema.decodeTo(
            Schema.optionalKey(Schema.String),
            {
              decode: SchemaGetter.required(),
              encode: SchemaGetter.withDefault(Effect.succeed("default"))
            }
          )
        )
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "a" })
      await decoding.fail(
        {},
        `Missing key
  at ["a"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed({ a: "a" })
      await encoding.succeed({}, { a: "default" })
    })

    it("optionalKey to required", async () => {
      const schema = Schema.Struct({
        a: Schema.optionalKey(Schema.String).pipe(
          Schema.decodeTo(
            Schema.String,
            {
              decode: SchemaGetter.withDefault(Effect.succeed("default")),
              encode: SchemaGetter.passthrough()
            }
          )
        )
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "a" })
      await decoding.succeed({}, { a: "default" })

      const encoding = asserts.encoding()
      await encoding.succeed({ a: "a" })
    })

    it("double transformation", async () => {
      const schema = Schema.Trim.pipe(Schema.decodeTo(
        Schema.FiniteFromString,
        SchemaTransformation.passthrough()
      ))
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(" 2 ", 2)
      await decoding.fail(
        " a2 ",
        `Expected a finite number, got NaN`
      )

      const encoding = asserts.encoding()
      await encoding.succeed(2, "2")
    })

    it("double transformation with checks", async () => {
      const schema = Schema.Struct({
        a: Schema.String.check(Schema.isMinLength(2)).pipe(
          Schema.decodeTo(
            Schema.String.check(Schema.isMinLength(3)),
            SchemaTransformation.passthrough()
          ),
          Schema.decodeTo(
            Schema.String,
            SchemaTransformation.passthrough()
          )
        )
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "aaa" })
      await decoding.fail(
        { a: "aa" },
        `Expected a value with a length of at least 3, got "aa"
  at ["a"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed({ a: "aaa" })
      await encoding.fail(
        { a: "aa" },
        `Expected a value with a length of at least 3, got "aa"
  at ["a"]`
      )
    })

    it("nested defaults", async () => {
      const schema = Schema.Struct({
        a: Schema.optionalKey(Schema.Struct({
          b: Schema.optionalKey(Schema.String)
        })).pipe(Schema.decodeTo(
          Schema.Struct({
            b: Schema.optionalKey(Schema.String).pipe(
              Schema.decodeTo(
                Schema.String,
                {
                  decode: SchemaGetter.withDefault(Effect.succeed("default-b")),
                  encode: SchemaGetter.passthrough()
                }
              )
            )
          }),
          {
            decode: SchemaGetter.withDefault(Effect.succeed({})),
            encode: SchemaGetter.passthrough()
          }
        ))
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: { b: "b" } })
      await decoding.succeed({ a: {} }, { a: { b: "default-b" } })
      await decoding.succeed({}, { a: { b: "default-b" } })
    })
  })

  describe("decode", () => {
    it("double transformation", async () => {
      const schema = Schema.String.pipe(
        Schema.decode(
          SchemaTransformation.trim().compose(
            SchemaTransformation.toLowerCase()
          )
        )
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(" A ", "a")

      const encoding = asserts.encoding()
      await encoding.succeed(" A ", " A ")
    })
  })

  describe("encodeTo", () => {
    it("required to required", async () => {
      const schema = Schema.Struct({
        a: Schema.String.pipe(
          Schema.encodeTo(
            Schema.String,
            SchemaTransformation.passthrough()
          )
        )
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "a" })
      await decoding.fail(
        {},
        `Missing key
  at ["a"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed({ a: "a" })
      await encoding.fail(
        {},
        `Missing key
  at ["a"]`
      )
    })

    it("required to optionalKey", async () => {
      const schema = Schema.Struct({
        a: Schema.String.pipe(
          Schema.encodeTo(
            Schema.optionalKey(Schema.String),
            {
              decode: SchemaGetter.withDefault(Effect.succeed("default")),
              encode: SchemaGetter.passthrough()
            }
          )
        )
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "a" })
      await decoding.succeed({}, { a: "default" })

      const encoding = asserts.encoding()
      await encoding.succeed({ a: "a" })
    })

    it("optionalKey to required", async () => {
      const schema = Schema.Struct({
        a: Schema.optionalKey(Schema.String).pipe(
          Schema.encodeTo(
            Schema.String,
            {
              decode: SchemaGetter.required(),
              encode: SchemaGetter.withDefault(Effect.succeed("default"))
            }
          )
        )
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "a" })
      await decoding.fail(
        {},
        `Missing key
  at ["a"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed({ a: "a" })
      await encoding.succeed({}, { a: "default" })
    })

    it("double transformation", async () => {
      const schema = Schema.FiniteFromString.pipe(Schema.encodeTo(
        Schema.Trim,
        SchemaTransformation.passthrough()
      ))
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(" 2 ", 2)
      await decoding.fail(
        " a2 ",
        `Expected a finite number, got NaN`
      )

      const encoding = asserts.encoding()
      await encoding.succeed(2, "2")
    })

    it("double transformation with checks", async () => {
      const schema = Schema.Struct({
        a: Schema.String.pipe(
          Schema.encodeTo(
            Schema.String.check(Schema.isMinLength(3)),
            SchemaTransformation.passthrough()
          ),
          Schema.encodeTo(
            Schema.String.check(Schema.isMinLength(2)),
            SchemaTransformation.passthrough()
          )
        )
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "aaa" })
      await decoding.fail(
        { a: "aa" },
        `Expected a value with a length of at least 3, got "aa"
  at ["a"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed({ a: "aaa" })
      await encoding.fail(
        { a: "aa" },
        `Expected a value with a length of at least 3, got "aa"
  at ["a"]`
      )
    })
  })

  describe("encode", () => {
    it("double transformation", async () => {
      const schema = Schema.String.pipe(
        Schema.encode(
          SchemaTransformation.trim().compose(
            SchemaTransformation.toLowerCase()
          ).flip()
        )
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(" A ", " A ")

      const encoding = asserts.encoding()
      await encoding.succeed(" A ", "a")
    })
  })

  describe("flip", () => {
    it("should expose the schema", () => {
      const schema = Schema.Struct({
        a: Schema.String
      })
      const flipped = schema.pipe(Schema.flip)
      strictEqual(flipped.schema, schema)
    })

    it("string & isMinLength(3) <-> number & isGreaterThan(2)", async () => {
      const schema = Schema.FiniteFromString.pipe(
        Schema.check(Schema.isGreaterThan(2)),
        Schema.flip,
        Schema.check(Schema.isMinLength(3))
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.fail(
        2,
        `Expected a value greater than 2, got 2`
      )
      await decoding.fail(
        3,
        `Expected a value with a length of at least 3, got "3"`
      )

      const encoding = asserts.encoding()
      await encoding.succeed("123", 123)
    })

    it("Struct & flip & check & flip should apply the check to the encoded side", async () => {
      const schema = Schema.Struct({ a: Schema.String }).pipe(
        Schema.flip,
        Schema.check(Schema.makeFilter((o) => o.a.length > 1, { expected: "a length > 1" })),
        Schema.flip
      )
      assertTrue(SchemaAST.isObjects(schema.ast))
      strictEqual(schema.ast.checks, undefined)
      strictEqual(schema.ast.encodingChecks?.length, 1)
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.fail(
        { a: "a" },
        `Expected a length > 1, got {"a":"a"}`
      )
      await decoding.succeed({ a: "aa" })

      const encoding = asserts.encoding()
      await encoding.fail(
        { a: "a" },
        `Expected a length > 1, got {"a":"a"}`
      )
      await encoding.succeed({ a: "aa" })
    })

    it("Tuple & flip & check & flip should apply the check to the encoded side", async () => {
      const schema = Schema.Tuple([Schema.String]).pipe(
        Schema.flip,
        Schema.check(Schema.makeFilter((tuple) => tuple[0].length > 1, { expected: "head length > 1" })),
        Schema.flip
      )
      assertTrue(SchemaAST.isArrays(schema.ast))
      strictEqual(schema.ast.checks, undefined)
      strictEqual(schema.ast.encodingChecks?.length, 1)
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.fail(
        ["a"],
        `Expected head length > 1, got ["a"]`
      )
      await decoding.succeed(["aa"])

      const encoding = asserts.encoding()
      await encoding.fail(
        ["a"],
        `Expected head length > 1, got ["a"]`
      )
      await encoding.succeed(["aa"])
    })

    it("Union & flip & check & flip should apply the check to the encoded side", async () => {
      const schema = Schema.Union([Schema.Literal("a"), Schema.Literal("aa")]).pipe(
        Schema.flip,
        Schema.check(Schema.makeFilter((s) => s === "aa", { expected: `"aa"` })),
        Schema.flip
      )
      assertTrue(SchemaAST.isUnion(schema.ast))
      strictEqual(schema.ast.checks, undefined)
      strictEqual(schema.ast.encodingChecks?.length, 1)
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.fail(
        "a",
        `Expected "aa", got "a"`
      )
      await decoding.succeed("aa")

      const encoding = asserts.encoding()
      await encoding.fail(
        "a",
        `Expected "aa", got "a"`
      )
      await encoding.succeed("aa")
    })

    it("Declaration & flip & check & flip should apply the check to the encoded side", async () => {
      const schema = Schema.declare(
        (u): u is string => typeof u === "string",
        { expected: "string declaration" }
      ).pipe(
        Schema.flip,
        Schema.check(Schema.makeFilter((s) => s.length > 1, { expected: "a length > 1" })),
        Schema.flip
      )
      assertTrue(SchemaAST.isDeclaration(schema.ast))
      strictEqual(schema.ast.checks, undefined)
      strictEqual(schema.ast.encodingChecks?.length, 1)
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.fail(
        "a",
        `Expected a length > 1, got "a"`
      )
      await decoding.succeed("aa")

      const encoding = asserts.encoding()
      await encoding.fail(
        "a",
        `Expected a length > 1, got "a"`
      )
      await encoding.succeed("aa")
    })

    it("Struct & flip & check & flip with encoding chain should check the local value", async () => {
      const local = Schema.Struct({ a: Schema.String }).pipe(
        Schema.flip,
        Schema.check(Schema.makeFilter((o) => typeof o.a === "string" && o.a.length > 1, {
          expected: "a length > 1"
        })),
        Schema.flip
      )
      const schema = Schema.Struct({ b: Schema.String }).pipe(
        Schema.decodeTo(local, {
          decode: SchemaGetter.transform<{ readonly a: string }, { readonly b: string }>((o) => ({ a: o.b })),
          encode: SchemaGetter.transform<{ readonly b: string }, { readonly a: string }>((o) => ({ b: o.a }))
        })
      )
      assertTrue(SchemaAST.isObjects(schema.ast))
      strictEqual(schema.ast.encoding?.length, 1)
      strictEqual(schema.ast.encodingChecks?.length, 1)
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.fail(
        { b: "a" },
        `Expected a length > 1, got {"a":"a"}`
      )
      await decoding.succeed({ b: "aa" }, { a: "aa" })

      const encoding = asserts.encoding()
      await encoding.fail(
        { a: "a" },
        `Expected a length > 1, got {"a":"a"}`
      )
      await encoding.succeed({ a: "aa" }, { b: "aa" })
    })

    it(`Struct & encoding chain & structural checks should check the local value with errors: "all"`, async () => {
      const local = Schema.Struct({ a: Schema.Finite }).check(Schema.isMaxProperties(1))
      const schema = Schema.Struct({ b: Schema.Number, c: Schema.String }).pipe(
        Schema.decodeTo(local, {
          decode: SchemaGetter.transform<
            { readonly a: number },
            { readonly b: number; readonly c: string }
          >((o) => ({ a: o.b })),
          encode: SchemaGetter.transform<
            { readonly b: number; readonly c: string },
            { readonly a: number }
          >((o) => ({ b: o.a, c: "" }))
        })
      )
      assertTrue(SchemaAST.isObjects(schema.ast))
      strictEqual(schema.ast.encoding?.length, 1)
      strictEqual(schema.ast.checks?.length, 1)
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding({ parseOptions: { errors: "all" } })
      await decoding.fail(
        { b: NaN, c: "extra" },
        `Expected a finite number, got NaN
  at ["a"]`
      )

      const encoding = asserts.encoding({ parseOptions: { errors: "all" } })
      await encoding.fail(
        { a: NaN },
        `Expected a finite number, got NaN
  at ["a"]`
      )
      await encoding.succeed({ a: 1 }, { b: 1, c: "" })
    })

    it("should work with withConstructorDefault", async () => {
      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(-1)))
      })
      const asserts = new TestSchema.Asserts(schema)

      const make = asserts.make()
      await make.succeed({ a: 1 })
      await make.succeed({}, { a: -1 })

      const flipped = schema.pipe(Schema.flip)
      const assertsFlipped = new TestSchema.Asserts(flipped)
      throws(() => flipped.make({} as any))
      const makeFlipped = assertsFlipped.make()
      await makeFlipped.succeed({ a: "1" })

      const flipped2 = flipped.pipe(Schema.flip)
      const assertsFlipped2 = new TestSchema.Asserts(flipped2)
      deepStrictEqual(flipped2.fields, schema.fields)
      const makeFlipped2 = assertsFlipped2.make()
      await makeFlipped2.succeed({ a: 1 })
      await makeFlipped2.succeed({}, { a: -1 })
    })
  })

  it("declare", async () => {
    const schema = Schema.declare(
      (u) => u instanceof File,
      { expected: "File" }
    )
    const asserts = new TestSchema.Asserts(schema)

    const decoding = asserts.decoding()
    await decoding.succeed(new File([], "a.txt"))
    await decoding.fail("a", `Expected File, got "a"`)
  })

  describe("Redacted", () => {
    it("should expose the value", () => {
      const schema = Schema.Redacted(Schema.String)
      strictEqual(schema.value, Schema.String)
      strictEqual(schema.annotate({}).value, Schema.String)
    })

    it("Redacted(Finite)", async () => {
      const schema = Schema.Redacted(Schema.Int)
      const asserts = new TestSchema.Asserts(schema)

      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const decoding = asserts.decoding()
      await decoding.succeed(Redacted.make(123))
      await decoding.fail(null, `Expected Redacted, got null`)
      await decoding.fail(
        Redacted.make("a"),
        `Invalid data <redacted>
  at ["value"]`
      )
      await decoding.fail(
        Redacted.make(1.2),
        `Invalid data <redacted>
  at ["value"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed(Redacted.make(123))
      await encoding.fail(null, `Expected Redacted, got null`)
      await encoding.fail(
        Redacted.make("a"),
        `Invalid data <redacted>
  at ["value"]`
      )
      await encoding.fail(
        Redacted.make(1.2),
        `Invalid data <redacted>
  at ["value"]`
      )
    })

    it("Redacted(FiniteFromString)", async () => {
      const schema = Schema.Redacted(Schema.FiniteFromString.check(Schema.isInt()))
      const asserts = new TestSchema.Asserts(schema)

      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const decoding = asserts.decoding()
      await decoding.succeed(Redacted.make("123"), Redacted.make(123))
      await decoding.fail(null, `Expected Redacted, got null`)
      await decoding.fail(
        Redacted.make(null),
        `Invalid data <redacted>
  at ["value"]`
      )
      await decoding.fail(
        Redacted.make("a"),
        `Invalid data <redacted>
  at ["value"]`
      )
      await decoding.fail(
        Redacted.make("1.2"),
        `Invalid data <redacted>
  at ["value"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed(Redacted.make(123), Redacted.make("123"))
      await encoding.fail(null, `Expected Redacted, got null`)
      await encoding.fail(
        Redacted.make(null),
        `Invalid data <redacted>
  at ["value"]`
      )
      await encoding.fail(
        Redacted.make("a"),
        `Invalid data <redacted>
  at ["value"]`
      )
      await encoding.fail(
        Redacted.make(1.2),
        `Invalid data <redacted>
  at ["value"]`
      )
    })

    it("with label", async () => {
      const schema = Schema.Redacted(Schema.NonEmptyString, { label: "password" })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(Redacted.make("a", { label: "password" }))
      await decoding.fail(
        Redacted.make("a", { label: "API key" }),
        `Expected "password", got "API key"
  at ["label"]`
      )
      await decoding.fail(
        Redacted.make(1, { label: "API key" }),
        `Expected "password", got "API key"
  at ["label"]`
      )
      await decoding.fail(
        Redacted.make(1, { label: "password" }),
        `Invalid data <redacted:password>
  at ["value"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed(Redacted.make("a", { label: "password" }))
      await encoding.fail(
        Redacted.make("a", { label: "API key" }),
        `Expected "password", got "API key"
  at ["label"]`
      )
      await encoding.fail(
        Redacted.make("", { label: "password" }),
        `Invalid data <redacted:password>
  at ["value"]`
      )
    })
  })

  describe("RedactedFromValue", () => {
    it("should not leak any information about the value", async () => {
      const schema = Schema.RedactedFromValue(Schema.Literal("secret"))
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.fail(null, `Invalid data <redacted>`)
    })

    it("should decode a value", async () => {
      const schema = Schema.RedactedFromValue(Schema.FiniteFromString.check(Schema.isInt()))
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("123", Redacted.make(123))
      await decoding.fail(null, `Invalid data <redacted>`)
      await decoding.fail("1.2", `Invalid data <redacted>`)
    })
  })

  describe("Option", () => {
    it("should expose the value", () => {
      const schema = Schema.Option(Schema.String)
      strictEqual(schema.value, Schema.String)
      strictEqual(schema.annotate({}).value, Schema.String)
    })

    it("Option(FiniteFromString)", async () => {
      const schema = Schema.Option(Schema.FiniteFromString)
      const asserts = new TestSchema.Asserts(schema)

      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const decoding = asserts.decoding()
      await decoding.succeed(Option.none())
      await decoding.succeed(Option.some("123"), Option.some(123))
      await decoding.fail(null, `Expected Option, got null`)
      await decoding.fail(
        Option.some(null),
        `Expected string, got null
  at ["value"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed(Option.none())
      await encoding.succeed(Option.some(123), Option.some("123"))
      await encoding.fail(null, `Expected Option, got null`)
      await encoding.fail(
        Option.some(null),
        `Expected number, got null
  at ["value"]`
      )
    })
  })

  it("OptionFromNullOr", async () => {
    const schema = Schema.OptionFromNullOr(Schema.FiniteFromString)
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed(null, Option.none())
    await decoding.succeed("1", Option.some(1))
    await decoding.fail("a", `Expected a finite number, got NaN`)

    const encoding = asserts.encoding()
    await encoding.succeed(Option.none(), null)
    await encoding.succeed(Option.some(1), "1")
  })

  it("OptionFromUndefinedOr", async () => {
    const schema = Schema.OptionFromUndefinedOr(Schema.FiniteFromString)
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed(undefined, Option.none())
    await decoding.succeed("1", Option.some(1))
    await decoding.fail("a", `Expected a finite number, got NaN`)

    const encoding = asserts.encoding()
    await encoding.succeed(Option.none(), undefined)
    await encoding.succeed(Option.some(1), "1")
  })

  describe("OptionFromNullishOr", () => {
    it("onNoneEncoding: null", async () => {
      const schema = Schema.OptionFromNullishOr(Schema.FiniteFromString, { onNoneEncoding: null })
      const asserts = new TestSchema.Asserts(schema)

      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const decoding = asserts.decoding()
      await decoding.succeed(null, Option.none())
      await decoding.succeed(undefined, Option.none())
      await decoding.succeed("1", Option.some(1))
      await decoding.fail("a", `Expected a finite number, got NaN`)

      const encoding = asserts.encoding()
      await encoding.succeed(Option.none(), null)
      await encoding.succeed(Option.some(1), "1")
    })

    it("onNoneEncoding: undefined", async () => {
      const schema = Schema.OptionFromNullishOr(Schema.FiniteFromString, { onNoneEncoding: undefined })
      const asserts = new TestSchema.Asserts(schema)

      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const decoding = asserts.decoding()
      await decoding.succeed(null, Option.none())
      await decoding.succeed(undefined, Option.none())
      await decoding.succeed("1", Option.some(1))
      await decoding.fail("a", `Expected a finite number, got NaN`)

      const encoding = asserts.encoding()
      await encoding.succeed(Option.none(), undefined)
      await encoding.succeed(Option.some(1), "1")
    })
  })

  it("OptionFromOptionalKey", async () => {
    const schema = Schema.Struct({
      a: Schema.OptionFromOptionalKey(Schema.FiniteFromString)
    })
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed({}, { a: Option.none() })
    await decoding.succeed({ a: "1" }, { a: Option.some(1) })
    await decoding.fail(
      { a: undefined },
      `Expected string, got undefined
  at ["a"]`
    )
    await decoding.fail(
      { a: "a" },
      `Expected a finite number, got NaN
  at ["a"]`
    )

    const encoding = asserts.encoding()
    await encoding.succeed({ a: Option.none() }, {})
    await encoding.succeed({ a: Option.some(1) }, { a: "1" })
  })

  it("OptionFromOptional", async () => {
    const schema = Schema.Struct({
      a: Schema.OptionFromOptional(Schema.FiniteFromString)
    })
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed({}, { a: Option.none() })
    await decoding.succeed({ a: undefined }, { a: Option.none() })
    await decoding.succeed({ a: "1" }, { a: Option.some(1) })
    await decoding.fail(
      { a: "a" },
      `Expected a finite number, got NaN
  at ["a"]`
    )

    const encoding = asserts.encoding()
    await encoding.succeed({ a: Option.none() }, {})
    await encoding.succeed({ a: Option.some(1) }, { a: "1" })
  })

  describe("OptionFromOptionalNullOr", () => {
    it("default (omit)", async () => {
      const schema = Schema.Struct({
        a: Schema.OptionFromOptionalNullOr(Schema.FiniteFromString)
      })
      const asserts = new TestSchema.Asserts(schema)

      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const decoding = asserts.decoding()
      await decoding.succeed({}, { a: Option.none() })
      await decoding.succeed({ a: null }, { a: Option.none() })
      await decoding.succeed({ a: undefined }, { a: Option.none() })
      await decoding.succeed({ a: "1" }, { a: Option.some(1) })
      await decoding.fail(
        { a: "a" },
        `Expected a finite number, got NaN
  at ["a"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed({ a: Option.none() }, {})
      await encoding.succeed({ a: Option.some(1) }, { a: "1" })
      await encoding.fail(
        { a: null },
        `Expected Option, got null
  at ["a"]`
      )
    })

    it("onNoneEncoding: null", async () => {
      const schema = Schema.Struct({
        a: Schema.OptionFromOptionalNullOr(Schema.FiniteFromString, { onNoneEncoding: null })
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({}, { a: Option.none() })
      await decoding.succeed({ a: null }, { a: Option.none() })
      await decoding.succeed({ a: undefined }, { a: Option.none() })
      await decoding.succeed({ a: "1" }, { a: Option.some(1) })

      const encoding = asserts.encoding()
      await encoding.succeed({ a: Option.none() }, { a: null })
      await encoding.succeed({ a: Option.some(1) }, { a: "1" })
    })

    it("onNoneEncoding: undefined", async () => {
      const schema = Schema.Struct({
        a: Schema.OptionFromOptionalNullOr(Schema.FiniteFromString, { onNoneEncoding: undefined })
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({}, { a: Option.none() })
      await decoding.succeed({ a: null }, { a: Option.none() })
      await decoding.succeed({ a: undefined }, { a: Option.none() })
      await decoding.succeed({ a: "1" }, { a: Option.some(1) })

      const encoding = asserts.encoding()
      await encoding.succeed({ a: Option.none() }, { a: undefined })
      await encoding.succeed({ a: Option.some(1) }, { a: "1" })
    })
  })

  describe("Result", () => {
    it("should expose the values", () => {
      const schema = Schema.Result(Schema.String, Schema.Number)
      strictEqual(schema.success, Schema.String)
      strictEqual(schema.annotate({}).success, Schema.String)
      strictEqual(schema.failure, Schema.Number)
      strictEqual(schema.annotate({}).failure, Schema.Number)
    })

    it("Result(FiniteFromString, FiniteFromString)", async () => {
      const schema = Schema.Result(Schema.FiniteFromString, Schema.FiniteFromString)
      const asserts = new TestSchema.Asserts(schema)

      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const decoding = asserts.decoding()
      await decoding.succeed(Result.succeed("1"), Result.succeed(1))
      await decoding.succeed(Result.fail("2"), Result.fail(2))
      await decoding.fail(null, `Expected Result, got null`)
      await decoding.fail(
        Result.succeed("a"),
        `Expected a finite number, got NaN
  at ["success"]`
      )
      await decoding.fail(
        Result.fail("b"),
        `Expected a finite number, got NaN
  at ["failure"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed(Result.succeed(1), Result.succeed("1"))
      await encoding.succeed(Result.fail(2), Result.fail("2"))
    })
  })

  it("Defect", async () => {
    const schema = Schema.Defect()
    const asserts = new TestSchema.Asserts(schema)

    const noPrototypeObject = Object.create(null)
    noPrototypeObject.message = "a"

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    // Error: message only
    await decoding.succeed({ message: "a" }, new Error("a"))
    await decoding.succeed(noPrototypeObject, new Error("a"))
    // Error: message and name
    await decoding.succeed(
      { message: "a", name: "b" },
      (() => {
        const err = new Error("a")
        err.name = "b"
        return err
      })()
    )
    // Error: message, name, and stack
    await decoding.succeed(
      { message: "a", name: "b", stack: "c" },
      (() => {
        const err = new Error("a")
        err.name = "b"
        err.stack = "c"
        return err
      })()
    )
    // anything else
    await decoding.succeed("a")
    await decoding.succeed({ a: 1 })

    const encoding = asserts.encoding()
    // Error
    await encoding.succeed(new Error("a"), { name: "Error", message: "a" })
    // anything else
    await encoding.succeed("a")
    await encoding.succeed({ a: 1 })
    await encoding.succeed(noPrototypeObject, { message: "a" })
  })

  it("Error and Defect memoize equivalent options", () => {
    const assertMemoized = <S>(schema: (options?: Schema.ErrorOptions) => S) => {
      strictEqual(schema(), schema({}))
      strictEqual(schema(), schema({ includeStack: false }))
      strictEqual(schema(), schema({ excludeCause: false }))
      strictEqual(schema(), schema({ includeStack: false, excludeCause: false }))
      strictEqual(schema({ includeStack: true }), schema({ includeStack: true }))
      strictEqual(schema({ includeStack: true }), schema({ includeStack: true, excludeCause: false }))
      strictEqual(schema({ excludeCause: true }), schema({ excludeCause: true }))
      strictEqual(schema({ excludeCause: true }), schema({ includeStack: false, excludeCause: true }))
      strictEqual(
        schema({ includeStack: true, excludeCause: true }),
        schema({ includeStack: true, excludeCause: true })
      )
      assertFalse(schema() === schema({ includeStack: true }))
      assertFalse(schema() === schema({ excludeCause: true }))
      assertFalse(schema({ includeStack: true }) === schema({ includeStack: true, excludeCause: true }))
      assertFalse(schema({ excludeCause: true }) === schema({ includeStack: true, excludeCause: true }))
    }

    assertMemoized(Schema.Error)
    assertMemoized(Schema.Defect)
  })

  describe("CauseReason", () => {
    it("should expose the values", () => {
      const schema = Schema.CauseReason(Schema.String, Schema.Number)
      strictEqual(schema.error, Schema.String)
      strictEqual(schema.annotate({}).error, Schema.String)
      strictEqual(schema.defect, Schema.Number)
      strictEqual(schema.annotate({}).defect, Schema.Number)
    })

    it("CauseReason(FiniteFromString, FiniteFromString)", async () => {
      const schema = Schema.CauseReason(Schema.FiniteFromString, Schema.FiniteFromString)
      const asserts = new TestSchema.Asserts(schema)

      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }
    })
  })

  describe("Cause", () => {
    it("should expose the values", () => {
      const schema = Schema.Cause(Schema.String, Schema.Number)
      strictEqual(schema.error, Schema.String)
      strictEqual(schema.annotate({}).error, Schema.String)
      strictEqual(schema.defect, Schema.Number)
      strictEqual(schema.annotate({}).defect, Schema.Number)
    })

    it("Cause(FiniteFromString, FiniteFromString)", async () => {
      const schema = Schema.Cause(Schema.FiniteFromString, Schema.FiniteFromString)
      const asserts = new TestSchema.Asserts(schema)

      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const decoding = asserts.decoding()
      await decoding.succeed(Cause.fail("1"), Cause.fail(1))
      await decoding.succeed(Cause.die("2"), Cause.die(2))
      await decoding.succeed(Cause.interrupt(3))

      await decoding.fail(
        Cause.fail("a"),
        `Expected a finite number, got NaN
  at ["failures"][0]["error"]`
      )
      await decoding.fail(
        Cause.die("a"),
        `Expected a finite number, got NaN
  at ["failures"][0]["defect"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed(Cause.fail(1), Cause.fail("1"))
      await encoding.succeed(Cause.die(2), Cause.die("2"))
      await encoding.succeed(Cause.interrupt(3))

      await encoding.fail(
        Cause.fail("a"),
        `Expected number, got "a"
  at ["failures"][0]["error"]`
      )
      await encoding.fail(
        Cause.die("a"),
        `Expected number, got "a"
  at ["failures"][0]["defect"]`
      )
    })
  })

  it("Error", async () => {
    const schema = Schema.Error()
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const error = new Error("a")
    const customError = new Error("b")
    customError.name = "CustomError"
    customError.stack = "stack"

    const decoding = asserts.decoding()
    await decoding.succeed(error)
    await decoding.succeed(customError)
    await decoding.fail(
      { message: "a" },
      `Expected Error, got {"message":"a"}`
    )
    await decoding.fail(
      "a",
      `Expected Error, got "a"`
    )

    const encoding = asserts.encoding()
    await encoding.succeed(error)
    await encoding.succeed(customError)
    await encoding.fail(
      { message: "a" },
      `Expected Error, got {"message":"a"}`
    )
    await encoding.fail(
      "a",
      `Expected Error, got "a"`
    )
  })

  describe("Exit", () => {
    it("should expose the values", () => {
      const schema = Schema.Exit(Schema.String, Schema.Number, Schema.Boolean)
      strictEqual(schema.value, Schema.String)
      strictEqual(schema.annotate({}).value, Schema.String)
      strictEqual(schema.error, Schema.Number)
      strictEqual(schema.annotate({}).error, Schema.Number)
      strictEqual(schema.defect, Schema.Boolean)
      strictEqual(schema.annotate({}).defect, Schema.Boolean)
    })

    it("Exit(FiniteFromString, String, Unknown)", async () => {
      const schema = Schema.Exit(Schema.FiniteFromString, Schema.String, Schema.Unknown)
      const asserts = new TestSchema.Asserts(schema)

      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const decoding = asserts.decoding()
      await decoding.succeed(Exit.succeed("123"), Exit.succeed(123))
      await decoding.succeed(Exit.fail("boom"))
      await decoding.fail(
        null,
        `Expected Exit, got null`
      )
      await decoding.fail(
        Exit.succeed(123),
        `Expected string, got 123
  at ["value"]`
      )
      await decoding.fail(
        Exit.fail(null),
        `Expected string, got null
  at ["cause"]["failures"][0]["error"]`
      )
    })

    it("Exit(FiniteFromString, String, Defect)", async () => {
      const schema = Schema.Exit(Schema.FiniteFromString, Schema.String, Schema.Defect())
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      const boomError = new Error("boom message")
      boomError.name = "boom"

      await decoding.succeed(
        Exit.die({
          name: "boom",
          message: "boom message"
        }),
        Exit.die(boomError)
      )
    })
  })

  describe("suspend", () => {
    it("should work", async () => {
      interface Category<A, T> {
        readonly a: A
        readonly categories: ReadonlyArray<T>
      }
      interface CategoryType extends Category<number, CategoryType> {}
      interface CategoryEncoded extends Category<string, CategoryEncoded> {}

      const schema = Schema.Struct({
        a: Schema.FiniteFromString.check(Schema.isGreaterThan(0)),
        categories: Schema.Array(Schema.suspend((): Schema.Codec<CategoryType, CategoryEncoded> => schema))
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "1", categories: [] }, { a: 1, categories: [] })
      await decoding.succeed({ a: "1", categories: [{ a: "2", categories: [] }] }, {
        a: 1,
        categories: [{ a: 2, categories: [] }]
      })
      await decoding.fail(
        {
          a: "1",
          categories: [{ a: "a", categories: [] }]
        },
        `Expected a finite number, got NaN
  at ["categories"][0]["a"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1, categories: [] }, { a: "1", categories: [] })
      await encoding.succeed({ a: 1, categories: [{ a: 2, categories: [] }] }, {
        a: "1",
        categories: [{ a: "2", categories: [] }]
      })
      await encoding.fail(
        { a: 1, categories: [{ a: -1, categories: [] }] },
        `Expected a value greater than 0, got -1
  at ["categories"][0]["a"]`
      )
    })
  })

  describe("make", () => {
    it("should throw an error when the cause contains both a schema issue and a defect", () => {
      const cause = Cause.combine(
        Cause.fail(new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" }))),
        Cause.die(new Error("defect"))
      )
      const schema = Schema.Struct({
        a: Schema.String.pipe(Schema.withConstructorDefault(Effect.failCause(cause)))
      })

      throws(() => schema.make({}), (e) => {
        assertTrue(e instanceof Error)
        strictEqual(e.message, "Constructor adapter can only throw schema issues")
        assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
      })
    })
  })

  describe("makeOption", () => {
    it("Struct", () => {
      const schema = Schema.Struct({ a: Schema.Number.check(Schema.isGreaterThan(0)) })
      deepStrictEqual(schema.makeOption({ a: 1 }), Option.some({ a: 1 }))
      deepStrictEqual(schema.makeOption({ a: -1 }), Option.none())
    })

    it("Class", () => {
      class A extends Schema.Class<A>("A")(Schema.Struct({ a: Schema.Number.check(Schema.isGreaterThan(0)) })) {}
      deepStrictEqual(A.makeOption({ a: 1 }), Option.some(new A({ a: 1 })))
      deepStrictEqual(A.makeOption({ a: -1 }), Option.none())
    })

    it("should throw an error when the cause is not a schema issue", () => {
      const schema = Schema.Struct({
        a: Schema.String.pipe(Schema.withConstructorDefault(Effect.die(new Error("make defect"))))
      })
      class A extends Schema.Class<A>("A")(schema) {}

      throws(() => schema.makeOption({}), (e) => {
        assertTrue(e instanceof Error)
        strictEqual(e.message, "Option adapter can only return none for schema issues")
        assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
      })
      throws(() => A.makeOption(), (e) => {
        assertTrue(e instanceof Error)
        strictEqual(e.message, "Option adapter can only return none for schema issues")
        assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
      })
    })
  })

  describe("makeEffect", () => {
    it.effect("Struct", () =>
      Effect.gen(function*() {
        const schema = Schema.Struct({ a: Schema.Number.check(Schema.isGreaterThan(0)) })

        const success = yield* schema.makeEffect({ a: 1 }).pipe(Effect.result)
        deepStrictEqual(success, Result.succeed({ a: 1 }))

        const failure = yield* schema.makeEffect({ a: -1 }).pipe(Effect.flip)
        assertTrue(Schema.isSchemaError(failure))
      }))

    it.effect("Class", () =>
      Effect.gen(function*() {
        class A extends Schema.Class<A>("A")(Schema.Struct({ a: Schema.Number.check(Schema.isGreaterThan(0)) })) {}

        const success = yield* A.makeEffect({ a: 1 })
        deepStrictEqual(success, new A({ a: 1 }))

        const failure = yield* A.makeEffect({ a: -1 }).pipe(Effect.flip)
        assertTrue(Schema.isSchemaError(failure))
      }))

    it.effect("should preserve mixed schema error and defect causes", () =>
      Effect.gen(function*() {
        const cause = Cause.combine(
          Cause.fail(
            new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" }))
          ),
          Cause.die(new Error("defect"))
        )
        const schema = Schema.Struct({
          a: Schema.String.pipe(Schema.withConstructorDefault(Effect.failCause(cause)))
        })

        const exit = yield* schema.makeEffect({}).pipe(Effect.exit)
        assertTrue(Exit.isFailure(exit))
        assertTrue(Exit.hasDies(exit))
        const error = Cause.findError(exit.cause)
        assertTrue(Result.isSuccess(error))
        assertTrue(Schema.isSchemaError(error.success))
      }))
  })

  describe("withConstructorDefault", () => {
    describe("Struct", () => {
      it("should not apply defaults when decoding / encoding", async () => {
        const schema = Schema.Struct({
          a: Schema.String.pipe(Schema.optionalKey, Schema.withConstructorDefault(Effect.succeed("a")))
        })
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed({}, {})

        const encoding = asserts.encoding()
        await encoding.succeed({}, {})
      })

      it("should apply constructor default when the field is not present", async () => {
        const schema = Schema.Struct({
          a: Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(-1)))
        })
        const asserts = new TestSchema.Asserts(schema)

        const make = asserts.make()
        await make.succeed({ a: 1 })
        await make.succeed({}, { a: -1 })
      })

      describe("nested defaults", () => {
        it("Struct", async () => {
          const schema = Schema.Struct({
            a: Schema.Struct({
              b: Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(-1)))
            }).pipe(Schema.withConstructorDefault(Effect.succeed({})))
          })
          const asserts = new TestSchema.Asserts(schema)

          const make = asserts.make()
          await make.succeed({ a: { b: 1 } })
          await make.succeed({ a: {} }, { a: { b: -1 } })
          await make.succeed({}, { a: { b: -1 } })
        })

        it("Class", async () => {
          class A extends Schema.Class<A>("A")(Schema.Struct({
            a: Schema.Struct({
              b: Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(-1)))
            }).pipe(Schema.withConstructorDefault(Effect.succeed({})))
          })) {}

          const asserts = new TestSchema.Asserts(A)

          const make = asserts.make()
          await make.succeed({ a: { b: 1 } }, new A({ a: { b: 1 } }))
          await make.succeed({ a: {} }, new A({ a: { b: -1 } }))
          await make.succeed({}, new A({ a: { b: -1 } }))

          deepStrictEqual(A.make({ a: { b: 1 } }), new A({ a: { b: 1 } }))
          deepStrictEqual(A.make({ a: {} }), new A({ a: { b: -1 } }))
          deepStrictEqual(A.make({}), new A({ a: { b: -1 } }))
        })
      })

      it("applies constructor default when disableChecks is true", () => {
        class A extends Schema.Class<A>("A")({
          a: Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed("default")))
        }) {}

        const instance = new A({}, { disableChecks: true })
        deepStrictEqual(instance.a, "default")
      })

      it("Struct & Effect sync", async () => {
        const schema = Schema.Struct({
          a: Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(-1)))
        })
        const asserts = new TestSchema.Asserts(schema)

        const make = asserts.make()
        await make.succeed({ a: 1 })
        await make.succeed({}, { a: -1 })
      })

      it("Struct & Effect async", async () => {
        const schema = Schema.Struct({
          a: Schema.FiniteFromString.pipe(Schema.withConstructorDefault(
            Effect.gen(function*() {
              yield* Effect.sleep(100)
              return -1
            })
          ))
        })
        const asserts = new TestSchema.Asserts(schema)

        const make = asserts.make()
        await make.succeed({ a: 1 })
        await make.succeed({}, { a: -1 })
      })

      it("Effect failing with SchemaError propagates as parse failure", async () => {
        const schema = Schema.Struct({
          a: Schema.FiniteFromString.pipe(Schema.withConstructorDefault(
            Effect.fail(
              new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.none(), { message: "ctor default failed" }))
            )
          ))
        })
        const asserts = new TestSchema.Asserts(schema)

        const make = asserts.make()
        await make.succeed({ a: 1 })
        await make.fail(
          {},
          `ctor default failed
  at ["a"]`
        )
      })

      it("Effect failing via another schema's makeEffect propagates as parse failure", async () => {
        const Inner = Schema.Struct({
          n: Schema.FiniteFromString.pipe(Schema.check(Schema.isGreaterThan(0)))
        })
        const schema = Schema.Struct({
          a: Schema.FiniteFromString.pipe(Schema.withConstructorDefault(
            Inner.makeEffect({ n: -1 }).pipe(Effect.map((s) => s.n))
          ))
        })
        const asserts = new TestSchema.Asserts(schema)

        const make = asserts.make()
        await make.succeed({ a: 1 })
        await make.fail(
          {},
          `Expected a value greater than 0, got -1
  at ["a"]["n"]`
        )
      })

      it("Struct & Effect async & service", async () => {
        class Service extends Context.Service<Service, { value: Effect.Effect<number> }>()("Service") {}

        const schema = Schema.Struct({
          a: Schema.FiniteFromString.pipe(Schema.withConstructorDefault(
            Effect.gen(function*() {
              yield* Effect.sleep(100)
              const oservice = yield* Effect.serviceOption(Service)
              if (Option.isNone(oservice)) {
                return -1
              }
              return yield* oservice.value.value
            })
          ))
        })
        const asserts = new TestSchema.Asserts(schema)

        const make = asserts.make()

        await make.succeed({ a: 1 })
        await make.succeed({}, { a: -1 })
        const effect = await schema.makeEffect({}).pipe(
          Effect.provideService(Service, { value: Effect.succeed(0) }),
          Effect.result,
          Effect.runPromise
        )
        deepStrictEqual(effect, Result.succeed({ a: 0 }))
      })
    })

    describe("Tuple", () => {
      it("Tuple & Some", async () => {
        const schema = Schema.Tuple(
          [Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(-1)))]
        )
        const asserts = new TestSchema.Asserts(schema)

        const make = asserts.make()
        await make.succeed([1])
        await make.succeed([], [-1])
      })

      it("nested defaults (Struct)", async () => {
        const schema = Schema.Tuple(
          [
            Schema.Struct({
              b: Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(-1)))
            }).pipe(Schema.withConstructorDefault(Effect.succeed({})))
          ]
        )
        const asserts = new TestSchema.Asserts(schema)

        const make = asserts.make()
        await make.succeed([{ b: 1 }])
        await make.succeed([{}], [{ b: -1 }])
        await make.succeed([], [{ b: -1 }])
      })

      it("inner defaults (Tuple)", async () => {
        const schema = Schema.Tuple(
          [
            Schema.Tuple([
              Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(-1)))
            ])
          ]
        )
        const asserts = new TestSchema.Asserts(schema)

        const make = asserts.make()
        await make.succeed([[1]])
        await make.succeed([[]], [[-1]])
      })

      it("nested defaults (Tuple)", async () => {
        const schema = Schema.Tuple(
          [
            Schema.Tuple([
              Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(-1)))
            ]).pipe(Schema.withConstructorDefault(Effect.succeed([] as const)))
          ]
        )
        const asserts = new TestSchema.Asserts(schema)

        const make = asserts.make()
        await make.succeed([[1]])
        await make.succeed([[]], [[-1]])
        await make.succeed([], [[-1]])
      })
    })
  })

  describe("Record", () => {
    it("Record(String, Number)", async () => {
      const schema = Schema.Record(Schema.String, Schema.Number)
      const asserts = new TestSchema.Asserts(schema)

      const make = asserts.make()
      await make.succeed({ a: 1 })
      await make.fail(null, `Expected object, got null`)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: 1 })
      await decoding.fail(null, "Expected object, got null")
      await decoding.fail(
        { a: "b" },
        `Expected number, got "b"
  at ["a"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1 })
      await encoding.fail(
        { a: "b" },
        `Expected number, got "b"
  at ["a"]`
      )
      await encoding.fail(null, "Expected object, got null")
    })

    it("Record(String, optionalKey(Number)) should throw", async () => {
      throws(
        () => Schema.Record(Schema.String, Schema.optionalKey(Schema.Number)),
        new Error("Cannot use `Schema.optionalKey` with index signatures, use `Schema.optional` instead.")
      )
    })

    it("Record(String, optional(Number))", async () => {
      const schema = Schema.Record(Schema.String, Schema.optional(Schema.Number))
      const asserts = new TestSchema.Asserts(schema)

      const make = asserts.make()
      await make.succeed({ a: 1 })
      await make.succeed({ a: undefined })
      await make.fail(null, `Expected object, got null`)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: 1 })
      await decoding.succeed({ a: undefined })

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1 })
      await encoding.succeed({ a: undefined })
    })

    it("Record(String.check, Number) should use the key checks to select keys", async () => {
      const schema = Schema.Record(Schema.String.check(Schema.isPattern(/^a/)), Schema.Number)
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: 1, ab: 2, b: "ignored" }, { a: 1, ab: 2 })
      await decoding.fail(
        { a: "bad", b: 1 },
        `Expected number, got "bad"
  at ["a"]`
      )
    })

    it("Record(Symbol, Number)", async () => {
      const schema = Schema.Record(Schema.Symbol, Schema.Number)
      const asserts = new TestSchema.Asserts(schema)

      const make = asserts.make()
      await make.succeed({ [Symbol.for("a")]: 1 })
      await make.fail(null, `Expected object, got null`)

      const decoding = asserts.decoding()
      await decoding.succeed({ [Symbol.for("a")]: 1 })
      await decoding.fail(null, "Expected object, got null")
      await decoding.fail(
        { [Symbol.for("a")]: "b" },
        `Expected number, got "b"
  at [Symbol(a)]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed({ [Symbol.for("a")]: 1 })
      await encoding.fail(
        { [Symbol.for("a")]: "b" },
        `Expected number, got "b"
  at [Symbol(a)]`
      )
      await encoding.fail(null, "Expected object, got null")
    })

    it("Record(Symbol.check, Number) should use the key checks to select keys", async () => {
      const a = Symbol.for("a")
      const b = Symbol.for("b")
      const schema = Schema.Record(
        Schema.Symbol.check(Schema.makeFilter((symbol) => Symbol.keyFor(symbol)?.startsWith("a") ?? false)),
        Schema.Number
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ [a]: 1, [b]: "ignored" }, { [a]: 1 })
      await decoding.fail(
        { [a]: "bad", [b]: 1 },
        `Expected number, got "bad"
  at [Symbol(a)]`
      )
    })

    it("Record(SnakeToCamel, NumberFromString)", async () => {
      const schema = Schema.Record(SnakeToCamel, Schema.NumberFromString)
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "1" }, { a: 1 })
      await decoding.succeed({ a_b: "1" }, { aB: 1 })
      await decoding.succeed({ a_b: "1", aB: "2" }, { aB: 2 })

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1 }, { a: "1" })
      await encoding.succeed({ aB: 1 }, { a_b: "1" })
      await encoding.succeed({ a_b: 1, aB: 2 }, { a_b: "2" })
    })

    it("Record(SnakeToCamel, Number, { keyValueCombiner: ... })", async () => {
      const schema = Schema.Record(SnakeToCamel, Schema.NumberFromString, {
        keyValueCombiner: {
          decode: {
            combine: ([_, v1], [k2, v2]) => [k2, v1 + v2]
          },
          encode: {
            combine: ([_, v1], [k2, v2]) => [k2, v1 + "e" + v2]
          }
        }
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "1" }, { a: 1 })
      await decoding.succeed({ a_b: "1" }, { aB: 1 })
      await decoding.succeed({ a_b: "1", aB: "2" }, { aB: 3 })

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1 }, { a: "1" })
      await encoding.succeed({ aB: 1 }, { a_b: "1" })
      await encoding.succeed({ a_b: 1, aB: 2 }, { a_b: "1e2" })
    })

    it("UniqueSymbol", async () => {
      const a = Symbol.for("a")
      const schema = Schema.Record(Schema.UniqueSymbol(a), Schema.Number)
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ [a]: 1 })
      await decoding.fail(
        { [a]: "b" },
        `Expected number, got "b"
  at [Symbol(a)]`
      )
    })

    describe("Literals keys", () => {
      it("Record(Literals, Number)", async () => {
        const schema = Schema.Record(Schema.Literals(["a", "b"]), Schema.Number)
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed({ a: 1, b: 2 })
        await decoding.fail(
          { a: 1 },
          `Missing key
  at ["b"]`
        )
        await decoding.fail(
          { b: 2 },
          `Missing key
  at ["a"]`
        )
      })

      it("Record(Literals, optionalKey(Number))", async () => {
        const schema = Schema.Record(Schema.Literals(["a", "b"]), Schema.optionalKey(Schema.Number))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed({})
        await decoding.succeed({ a: 1 })
        await decoding.succeed({ b: 2 })
        await decoding.succeed({ a: 1, b: 2 })
      })

      it("Record(Literals, mutableKey(Number))", async () => {
        const schema = Schema.Record(Schema.Literals(["a", "b"]), Schema.mutableKey(Schema.Number))
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed({ a: 1, b: 2 })
      })

      it("Record(Literals, mutableKey(optionalKey(Number)))", async () => {
        const schema = Schema.Record(
          Schema.Literals(["a", "b"]),
          Schema.mutableKey(Schema.optionalKey(Schema.Number))
        )
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.succeed({})
        await decoding.succeed({ a: 1 })
        await decoding.succeed({ b: 2 })
        await decoding.succeed({ a: 1, b: 2 })
      })
    })

    it("Record(Number, String)", async () => {
      const schema = Schema.Record(Schema.Number, Schema.FiniteFromString)
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ 1: "1", 2.2: "2", Infinity: "3", NaN: "4", "-Infinity": "5" }, {
        "1": 1,
        "2.2": 2,
        Infinity: 3,
        NaN: 4,
        "-Infinity": 5
      })
      await decoding.fail(
        { 1: null },
        `Expected string, got null
  at ["1"]`
      )
      await decoding.fail(
        { 1: "a" },
        `Expected a finite number, got NaN
  at ["1"]`
      )
    })

    it("Record(Int, String)", async () => {
      const schema = Schema.Record(Schema.Int, Schema.FiniteFromString)
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ 1: "1" }, { "1": 1 })
      await decoding.succeed({ 1: "1", "1.1": "ignored", Infinity: "ignored", NaN: "ignored" }, { "1": 1 })
      await decoding.fail(
        { 1: null },
        `Expected string, got null
  at ["1"]`
      )
      await decoding.fail(
        { 1: "a" },
        `Expected a finite number, got NaN
  at ["1"]`
      )
    })

    it("Record(TemplateLiteral with checked part, Number) should use the part checks to select keys", async () => {
      const schema = Schema.Record(Schema.TemplateLiteral(["a", Schema.NonEmptyString]), Schema.Number)
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "ignored", ab: 1 }, { ab: 1 })
      await decoding.fail(
        { a: 1, ab: "bad" },
        `Expected number, got "bad"
  at ["ab"]`
      )
    })

    it(`Record(Union(Number, "a"), FiniteFromString)`, async () => {
      const schema = Schema.Record(Schema.Union([Schema.Number, Schema.Literal("a")]), Schema.FiniteFromString)
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(
        { a: "-1", 1: "1", 2.2: "2", Infinity: "3", NaN: "4", "-Infinity": "5" },
        {
          a: -1,
          "1": 1,
          "2.2": 2,
          Infinity: 3,
          NaN: 4,
          "-Infinity": 5
        }
      )
    })
  })

  describe("Union", () => {
    it("empty", async () => {
      const schema = Schema.Union([])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.fail(null, `Expected never, got null`)
    })

    it(`String`, async () => {
      const schema = Schema.Union([Schema.String])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a")
      await decoding.fail(null, `Expected string, got null`)
    })

    it(`Void`, async () => {
      const schema = Schema.Union([Schema.Void])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(undefined)
      await decoding.succeed(null, undefined)
      await decoding.succeed("a", undefined)
      await decoding.succeed(1, undefined)
      await decoding.succeed({}, undefined)
      await decoding.succeed([], undefined)
    })

    it(`Void | String`, async () => {
      const schema = Schema.Union([Schema.Void, Schema.String])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a", undefined)
    })

    it(`String | Number`, async () => {
      const schema = Schema.Union([Schema.String, Schema.Number])
      const asserts = new TestSchema.Asserts(schema)

      deepStrictEqual(schema.members, [Schema.String, Schema.Number])

      const decoding = asserts.decoding()
      await decoding.succeed("a")
      await decoding.succeed(1)
      await decoding.fail(
        null,
        `Expected string | number, got null`
      )
    })

    it(`String | Never`, async () => {
      const schema = Schema.Union([Schema.String, Schema.Never])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a")
      await decoding.fail(null, `Expected string | never, got null`)
    })

    it(`String & isMinLength(1) | number & isGreaterThan(0)`, async () => {
      const schema = Schema.Union([
        Schema.NonEmptyString,
        Schema.Number.check(Schema.isGreaterThan(0))
      ])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a")
      await decoding.succeed(1)
      await decoding.fail(
        "",
        `Expected a value with a length of at least 1, got ""`
      )
      await decoding.fail(
        -1,
        `Expected a value greater than 0, got -1`
      )
    })

    it(`mode: "oneOf"`, async () => {
      const schema = Schema.Union([
        Schema.Struct({ a: Schema.String }),
        Schema.Struct({ b: Schema.Number })
      ], { mode: "oneOf" })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "a" })
      await decoding.succeed({ b: 1 })
      await decoding.fail(
        { a: "a", b: 1 },
        `Expected exactly one member to match the input {"a":"a","b":1}`
      )
    })

    it(`mode: "oneOf" with different sentinel keys`, async () => {
      const schema = Schema.Union([
        Schema.Struct({ kind: Schema.Literal("a"), value: Schema.String }),
        Schema.Struct({ status: Schema.Literal("ready"), value: Schema.String })
      ], { mode: "oneOf" })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.fail(
        { kind: "a", status: "ready", value: "value" },
        `Expected exactly one member to match the input {"kind":"a","status":"ready","value":"value"}`
      )
    })

    it(`mode: "oneOf" counts repeated member occurrences`, async () => {
      const member = Schema.Struct({ kind: Schema.Literal("a") })
      const schema = Schema.Union([member, member], { mode: "oneOf" })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.fail(
        { kind: "a" },
        `Expected exactly one member to match the input {"kind":"a"}`
      )
    })

    it("preserves member order after sentinel dispatch", async () => {
      const fallback = Schema.Struct({ value: Schema.String }).pipe(
        Schema.decodeTo(Schema.String, {
          decode: SchemaGetter.transform(() => "fallback"),
          encode: SchemaGetter.transform((value) => ({ value }))
        })
      )
      const discriminated = Schema.Struct({ kind: Schema.Literal("a"), value: Schema.String }).pipe(
        Schema.decodeTo(Schema.String, {
          decode: SchemaGetter.transform(() => "discriminated"),
          encode: SchemaGetter.transform((value) => ({ kind: "a" as const, value }))
        })
      )
      const schema = Schema.Union([fallback, discriminated])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ kind: "a", value: "value" }, "fallback")
    })

    it.effect("preserves member order with concurrent decoding", () =>
      Effect.gen(function*() {
        const firstLatch = yield* Deferred.make<void>()
        const secondCompleted = yield* Deferred.make<void>()
        const first = Schema.String.pipe(
          Schema.decode({
            decode: SchemaGetter.transformOrFail(() => Deferred.await(firstLatch).pipe(Effect.as("first"))),
            encode: SchemaGetter.passthrough()
          })
        )
        const second = Schema.String.pipe(
          Schema.decode({
            decode: SchemaGetter.transformOrFail(() =>
              Deferred.succeed(secondCompleted, undefined).pipe(Effect.as("second"))
            ),
            encode: SchemaGetter.passthrough()
          })
        )
        const fiber = yield* Schema.decodeUnknownEffect(Schema.Union([first, second]))("value", {
          concurrency: 2
        }).pipe(Effect.forkChild)

        yield* Deferred.await(secondCompleted)
        yield* Effect.yieldNow
        yield* Deferred.succeed(firstLatch, undefined)
        strictEqual(yield* Fiber.join(fiber), "first")
      }))

    it.effect("uses a buffered concurrent success after earlier candidates fail", () =>
      Effect.gen(function*() {
        const firstLatch = yield* Deferred.make<void>()
        const secondCompleted = yield* Deferred.make<void>()
        const first = Schema.String.pipe(
          Schema.decode({
            decode: SchemaGetter.transformOrFail((value) =>
              Deferred.await(firstLatch).pipe(
                Effect.andThen(Effect.fail(new SchemaIssue.Forbidden(Option.some(value), { message: "first failed" })))
              )
            ),
            encode: SchemaGetter.passthrough()
          })
        )
        const second = Schema.String.pipe(
          Schema.decode({
            decode: SchemaGetter.transformOrFail(() =>
              Deferred.succeed(secondCompleted, undefined).pipe(Effect.as("second"))
            ),
            encode: SchemaGetter.passthrough()
          })
        )
        const fiber = yield* Schema.decodeUnknownEffect(Schema.Union([first, second]))("value", {
          concurrency: 2
        }).pipe(Effect.forkChild)

        yield* Deferred.await(secondCompleted)
        yield* Effect.yieldNow
        yield* Deferred.succeed(firstLatch, undefined)
        strictEqual(yield* Fiber.join(fiber), "second")
      }))

    it.effect(`mode: "oneOf" detects concurrent successes in member order`, () =>
      Effect.gen(function*() {
        const firstLatch = yield* Deferred.make<void>()
        const secondCompleted = yield* Deferred.make<void>()
        const first = Schema.String.pipe(
          Schema.decode({
            decode: SchemaGetter.transformOrFail(() => Deferred.await(firstLatch).pipe(Effect.as("first"))),
            encode: SchemaGetter.passthrough()
          })
        )
        const second = Schema.String.pipe(
          Schema.decode({
            decode: SchemaGetter.transformOrFail(() =>
              Deferred.succeed(secondCompleted, undefined).pipe(Effect.as("second"))
            ),
            encode: SchemaGetter.passthrough()
          })
        )
        const fiber = yield* Schema.decodeUnknownEffect(Schema.Union([first, second], { mode: "oneOf" }))(
          "value",
          { concurrency: 2 }
        ).pipe(Effect.exit, Effect.forkChild)

        yield* Deferred.await(secondCompleted)
        yield* Effect.yieldNow
        yield* Deferred.succeed(firstLatch, undefined)
        const exit = yield* Fiber.join(fiber)
        strictEqual(exit._tag, "Failure")
        if (exit._tag === "Failure") {
          const reason = exit.cause.reasons[0]
          strictEqual(reason._tag, "Fail")
          if (reason._tag === "Fail") {
            assertTrue(Schema.isSchemaError(reason.error))
            if (Schema.isSchemaError(reason.error)) {
              strictEqual(reason.error.issue._tag, "OneOf")
              if (reason.error.issue._tag === "OneOf") {
                deepStrictEqual(reason.error.issue.successes, [first.ast, second.ast])
              }
            }
          }
        }
      }))

    it.effect("interrupts pending concurrent members after anyOf succeeds", () =>
      Effect.gen(function*() {
        const firstStarted = yield* Deferred.make<void>()
        const firstLatch = yield* Deferred.make<void>()
        const secondStarted = yield* Deferred.make<void>()
        const secondInterrupted = yield* Deferred.make<void>()
        const first = Schema.String.pipe(
          Schema.decode({
            decode: SchemaGetter.transformOrFail(() =>
              Deferred.succeed(firstStarted, undefined).pipe(
                Effect.andThen(Deferred.await(firstLatch)),
                Effect.as("first")
              )
            ),
            encode: SchemaGetter.passthrough()
          })
        )
        const second = Schema.String.pipe(
          Schema.decode({
            decode: SchemaGetter.transformOrFail(() =>
              Deferred.succeed(secondStarted, undefined).pipe(
                Effect.andThen(Effect.never),
                Effect.onInterrupt(() => Deferred.succeed(secondInterrupted, undefined).pipe(Effect.asVoid))
              )
            ),
            encode: SchemaGetter.passthrough()
          })
        )
        const fiber = yield* Schema.decodeUnknownEffect(Schema.Union([first, second]))("value", {
          concurrency: 2
        }).pipe(Effect.forkChild)

        yield* Deferred.await(firstStarted)
        yield* Deferred.await(secondStarted)
        yield* Deferred.succeed(firstLatch, undefined)
        strictEqual(yield* Fiber.join(fiber), "first")
        yield* Deferred.await(secondInterrupted)
      }))

    it(`mode: "oneOf" with Void`, async () => {
      const schema = Schema.Union([Schema.Void, Schema.String], { mode: "oneOf" })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.fail("a", `Expected exactly one member to match the input "a"`)
    })

    it("{} & Literal", async () => {
      const schema = Schema.Union([
        Schema.Struct({}),
        Schema.Literal("a")
      ])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed([])
    })

    describe("should exclude members based on failed sentinels", () => {
      it("string | struct", async () => {
        const schema = Schema.Union([
          Schema.String,
          Schema.Struct({ _tag: Schema.Literal("a"), a: Schema.String })
        ])
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.fail(
          {},
          `Expected string | { readonly "_tag": "a", ... }, got {}`
        )
      })

      it("string | tuple", async () => {
        const schema = Schema.Union([
          Schema.String,
          Schema.Tuple([Schema.Literal("a"), Schema.String])
        ])
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.fail(
          [],
          `Expected string | readonly [ "a", ... ], got []`
        )
      })

      it("tagged struct union", async () => {
        const schema = Schema.Union([
          Schema.Struct({ _tag: Schema.Literal("a"), a: Schema.String }),
          Schema.Struct({ _tag: Schema.Literal("b"), b: Schema.Number })
        ])
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.fail(
          { _tag: "a" },
          `Missing key
  at ["a"]`
        )
        await decoding.fail(
          { _tag: "b" },
          `Missing key
  at ["b"]`
        )
        await decoding.fail(
          { _tag: "c" },
          `Expected { readonly "_tag": "a", ... } | { readonly "_tag": "b", ... }, got {"_tag":"c"}`
        )
      })

      it("tagged tuple union", async () => {
        const schema = Schema.Union([
          Schema.Tuple([Schema.Literal("a"), Schema.String]),
          Schema.Tuple([Schema.Literal("b"), Schema.Number])
        ])
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.fail(
          ["a"],
          `Missing key
  at [1]`
        )
        await decoding.fail(
          ["b"],
          `Missing key
  at [1]`
        )
        await decoding.fail(
          ["c"],
          `Expected readonly [ "a", ... ] | readonly [ "b", ... ], got ["c"]`
        )
      })
    })
  })

  describe("TupleWithRest", () => {
    it("A required element cannot follow an optional element", () => {
      throws(
        () =>
          Schema.TupleWithRest(
            Schema.Tuple([Schema.optionalKey(Schema.String)]),
            [Schema.Boolean, Schema.String]
          ),
        new Error("A required element cannot follow an optional element. ts(1257)")
      )
      throws(
        () =>
          Schema.TupleWithRest(
            Schema.Tuple([Schema.optional(Schema.String)]),
            [Schema.Boolean, Schema.String]
          ),
        new Error("A required element cannot follow an optional element. ts(1257)")
      )
    })

    it("An optional element cannot follow a rest element", () => {
      throws(
        () =>
          Schema.TupleWithRest(
            Schema.Tuple([]),
            [Schema.Boolean, Schema.optionalKey(Schema.String)]
          ),
        new Error("An optional element cannot follow a rest element. ts(1266)")
      )
      throws(
        () =>
          Schema.TupleWithRest(
            Schema.Tuple([]),
            [Schema.Boolean, Schema.optional(Schema.String)]
          ),
        new Error("An optional element cannot follow a rest element. ts(1266)")
      )
    })

    it("[FiniteFromString, String] + [Boolean, String]", async () => {
      const schema = Schema.TupleWithRest(
        Schema.Tuple([Schema.FiniteFromString, Schema.String]),
        [Schema.Boolean, Schema.String]
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(["1", "a", "b"], [1, "a", "b"])
      await decoding.succeed(["1", "a", true, "b"], [1, "a", true, "b"])
      await decoding.succeed(["1", "a", true, true, "b"], [1, "a", true, true, "b"])
      await decoding.fail(
        ["1", "a"],
        `Missing key
  at [2]`
      )
      await decoding.fail(
        ["1", "a", true],
        `Expected string, got true
  at [2]`
      )
      await decoding.fail(
        ["1", "a", "b", "c"],
        `Expected boolean, got "b"
  at [2]`
      )
      await decoding.fail(
        ["1", "a", true, "b", "c"],
        `Expected boolean, got "b"
  at [3]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed([1, "a", "b"], ["1", "a", "b"])
      await encoding.succeed([1, "a", true, "b"], ["1", "a", true, "b"])
    })

    it("[FiniteFromString, String] + [Boolean, String, Number]", async () => {
      const schema = Schema.TupleWithRest(
        Schema.Tuple([Schema.FiniteFromString, Schema.String]),
        [Schema.Boolean, Schema.String, Schema.FiniteFromString]
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(["1", "a", "b", "2"], [1, "a", "b", 2])
      await decoding.succeed(["1", "a", true, "b", "2"], [1, "a", true, "b", 2])
      await decoding.succeed(["1", "a", true, true, "b", "2"], [1, "a", true, true, "b", 2])
      await decoding.fail(
        ["1", "a"],
        `Missing key
  at [2]`
      )
      await decoding.fail(
        ["1", "a", "b"],
        `Missing key
  at [3]`
      )
      await decoding.fail(
        ["1", "a", "b", "c"],
        `Expected a finite number, got NaN
  at [3]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed([1, "a", "b", 2], ["1", "a", "b", "2"])
      await encoding.succeed([1, "a", true, "b", 2], ["1", "a", true, "b", "2"])
    })

    it("[String] + [Boolean, String, Number, Number] validates every post-rest index", async () => {
      const schema = Schema.TupleWithRest(
        Schema.Tuple([Schema.String]),
        [Schema.Boolean, Schema.String, Schema.FiniteFromString, Schema.FiniteFromString]
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.fail(
        ["a", true, "b", "1", "x"],
        `Expected a finite number, got NaN
  at [4]`
      )
      await decoding.succeed(["a", true, "b", "1", "2"], ["a", true, "b", 1, 2])
    })
  })

  describe("StructWithRest", () => {
    it("should throw an error if there are encodings", () => {
      throws(
        () =>
          Schema.StructWithRest(
            Schema.Struct({}).pipe(Schema.encodeTo(Schema.Struct({}))),
            [Schema.Record(Schema.String, Schema.Number)]
          ),
        new Error(`StructWithRest does not support encodings`)
      )
    })

    it("Record(String, Number)", async () => {
      const schema = Schema.StructWithRest(
        Schema.Struct({ a: Schema.Number }),
        [Schema.Record(Schema.String, Schema.Number)]
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: 1 })
      await decoding.succeed({ a: 1, b: 2 })
      await decoding.fail(
        { a: 1, b: "" },
        `Expected number, got ""
  at ["b"]`
      )
    })

    it("Record(Symbol, Number)", async () => {
      const schema = Schema.StructWithRest(
        Schema.Struct({ a: Schema.Number }),
        [Schema.Record(Schema.Symbol, Schema.Number)]
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: 1 })
      await decoding.succeed({ a: 1, [Symbol.for("b")]: 2 })
      await decoding.fail(
        { a: 1, [Symbol.for("b")]: "c" },
        `Expected number, got "c"
  at [Symbol(b)]`
      )
    })

    it("Record(`a${string}`, Number)", async () => {
      const schema = Schema.StructWithRest(
        Schema.Struct({ a: Schema.Number }),
        [Schema.Record(Schema.TemplateLiteral(["a", Schema.String]), Schema.Finite)]
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: 1 })
      await decoding.succeed({ a: 1, "ab": 2 })
      await decoding.fail(
        { a: NaN, "ab": 2 },
        `Expected a finite number, got NaN
  at ["a"]`
      )
      await decoding.fail(
        { a: 1, "ab": "c" },
        `Expected number, got "c"
  at ["ab"]`
      )
    })

    it("should preserve both checks", async () => {
      const schema = Schema.StructWithRest(
        Schema.Struct({ a: Schema.Number }).check(
          Schema.makeFilter((s) => s.a > 0, { expected: "agt(0)" })
        ),
        [
          Schema.Record(Schema.String, Schema.Number).check(
            Schema.makeFilter((s) => s.b === undefined || s.b > 1, { expected: "bgt(1)" })
          )
        ]
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: 1 })
      await decoding.succeed({ a: 1, b: 2 })
      await decoding.fail(
        { a: 0 },
        `Expected agt(0), got {"a":0}`
      )
      await decoding.fail(
        { a: 1, b: 1 },
        `Expected bgt(1), got {"a":1,"b":1}`
      )
    })

    it("index signatures should not re-parse fixed properties", async () => {
      const Trimmed = Schema.String.pipe(Schema.decodeTo(Schema.String, SchemaTransformation.trim()))
      const schema = Schema.StructWithRest(
        Schema.Struct({ a: Trimmed }),
        [Schema.Record(Schema.String, Schema.String)]
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "  x  ", b: "y" }, { a: "x", b: "y" })

      const encoding = asserts.encoding()
      await encoding.succeed({ a: "x", b: "y" })
    })

    it("index signatures should not overwrite fixed properties after key decoding", async () => {
      const UppercaseKey = Schema.String.pipe(Schema.decodeTo(Schema.String, SchemaTransformation.toUpperCase()))
      const schema = Schema.StructWithRest(
        Schema.Struct({ A: Schema.String }),
        [Schema.Record(UppercaseKey, Schema.String)]
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ A: "fixed", a: "rest" }, { A: "fixed" })
    })
  })

  describe("NullOr", () => {
    it("NullOr(String)", async () => {
      const schema = Schema.NullOr(Schema.NonEmptyString)
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a")
      await decoding.succeed(null)
      await decoding.fail(undefined, `Expected string | null, got undefined`)
      await decoding.fail(
        "",
        `Expected a value with a length of at least 1, got ""`
      )
    })
  })

  describe("UndefinedOr", () => {
    it("UndefinedOr(String)", async () => {
      const schema = Schema.UndefinedOr(Schema.NonEmptyString)
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a")
      await decoding.succeed(undefined)
      await decoding.fail(null, `Expected string | undefined, got null`)
      await decoding.fail(
        "",
        `Expected a value with a length of at least 1, got ""`
      )
    })
  })

  describe("NullishOr", () => {
    it("NullishOr(String)", async () => {
      const schema = Schema.NullishOr(Schema.NonEmptyString)
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a")
      await decoding.succeed(null)
      await decoding.succeed(undefined)
      await decoding.fail(
        "",
        `Expected a value with a length of at least 1, got ""`
      )
    })
  })

  it("PropertyKey", async () => {
    const schema = Schema.PropertyKey
    const asserts = new TestSchema.Asserts(schema)
    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }
  })

  it("BooleanFromBit", async () => {
    const schema = Schema.BooleanFromBit
    const asserts = new TestSchema.Asserts(schema)
    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }
  })

  it("Uint8Array", async () => {
    const schema = Schema.Uint8Array
    const asserts = new TestSchema.Asserts(schema)
    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }
  })

  it("Uint8ArrayFromBase64", async () => {
    const schema = Schema.Uint8ArrayFromBase64
    const asserts = new TestSchema.Asserts(schema)
    const encoder = new TextEncoder()

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed("Zm9vYmFy", encoder.encode("foobar"))
    await decoding.fail("Zm9vY", "Length must be a multiple of 4, but is 5")
    await decoding.fail("Zm9vYmF-", "Invalid character -")
    await decoding.fail("=Zm9vYmF", "Found a '=' character, but it is not at the end")

    const encoding = asserts.encoding()
    await encoding.succeed(encoder.encode("foobar"), "Zm9vYmFy")
  })

  it("StringFromBase64", async () => {
    const schema = Schema.StringFromBase64
    const asserts = new TestSchema.Asserts(schema)

    const decoding = asserts.decoding()
    await decoding.succeed("Zm9vYmFy", "foobar")
    await decoding.fail("Zm9vY", "Length must be a multiple of 4, but is 5")
    await decoding.fail("Zm9vYmF-", "Invalid character -")
    await decoding.fail("=Zm9vYmF", "Found a '=' character, but it is not at the end")

    const encoding = asserts.encoding()
    await encoding.succeed("foobar", "Zm9vYmFy")
  })

  it("StringFromBase64Url", async () => {
    const schema = Schema.StringFromBase64Url
    const asserts = new TestSchema.Asserts(schema)

    const decoding = asserts.decoding()
    await decoding.succeed("Zm9vYmFy", "foobar")
    await decoding.fail("Zm9vY", "Length should be a multiple of 4, but is 5")
    await decoding.succeed("Pj8-ZD_Dnw", ">?>d?\u00DF")
    await decoding.fail("Pj8/ZD+Dnw", "Invalid input")

    const encoding = asserts.encoding()
    await encoding.succeed("foobar", "Zm9vYmFy")
    await encoding.succeed(">?>d?\u00DF", "Pj8-ZD_Dnw")
  })

  it("StringFromHex", async () => {
    const schema = Schema.StringFromHex
    const asserts = new TestSchema.Asserts(schema)

    const decoding = asserts.decoding()
    await decoding.succeed("67", "g")
    await decoding.fail("0", "Length must be a multiple of 2, but is 1")
    await decoding.fail("zd4aa", "Length must be a multiple of 2, but is 5")
    await decoding.fail("0\x01", "Invalid input")

    const encoding = asserts.encoding()
    await encoding.succeed("g", "67")
  })

  it("StringFromUriComponent", async () => {
    const schema = Schema.StringFromUriComponent
    const asserts = new TestSchema.Asserts(schema)

    const decoding = asserts.decoding()
    await decoding.succeed("%7B%22a%22%3A1%7D", "{\"a\":1}")
    await decoding.succeed("%D1%88%D0%B5%D0%BB%D0%BB%D1%8B", "шеллы")
    await decoding.succeed("hello%20world", "hello world")
    await decoding.succeed("hello", "hello")
    await decoding.fail("%ZZ", `URI malformed`)

    const encoding = asserts.encoding()
    await encoding.succeed("{\"a\":1}", "%7B%22a%22%3A1%7D")
    await encoding.succeed("hello world", "hello%20world")
  })

  it("Uint8ArrayFromBase64Url", async () => {
    const schema = Schema.Uint8ArrayFromBase64Url
    const asserts = new TestSchema.Asserts(schema)
    const encoder = new TextEncoder()

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed("Zm9vYmFy", encoder.encode("foobar"))
    await decoding.fail("Zm9vY", "Length should be a multiple of 4, but is 5")
    await decoding.succeed("Pj8-ZD_Dnw", encoder.encode(">?>d?ß"))
    await decoding.fail("Pj8/ZD+Dnw", "Invalid input")

    const encoding = asserts.encoding()
    await encoding.succeed(encoder.encode("foobar"), "Zm9vYmFy")
    await encoding.succeed(encoder.encode(">?>d?ß"), "Pj8-ZD_Dnw")
  })

  it("Uint8ArrayFromHex", async () => {
    const schema = Schema.Uint8ArrayFromHex
    const asserts = new TestSchema.Asserts(schema)
    const encoder = new TextEncoder()

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed(
      "0001020304050607",
      Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7])
    )
    await decoding.succeed(
      "f0f1f2f3f4f5f6f7",
      Uint8Array.from([0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7])
    )
    await decoding.succeed("67", encoder.encode("g"))
    await decoding.fail("0", "Length must be a multiple of 2, but is 1")
    await decoding.fail("2d4aa", "Length must be a multiple of 2, but is 5")
    await decoding.fail("0\x01", "Invalid input")

    const encoding = asserts.encoding()
    await encoding.succeed(Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7]), "0001020304050607")
  })

  it("Date", async () => {
    const schema = Schema.Date
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed(new Date("2021-01-01"))
    await decoding.fail(null, `Expected Date, got null`)
    await decoding.fail(0, `Expected Date, got 0`)
  })

  it("DateValid", async () => {
    const schema = Schema.DateValid
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }
  })

  it("DateTimeUtc", async () => {
    const schema = Schema.DateTimeUtc
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed(DateTime.makeUnsafe("2021-01-01T00:00:00.000Z"))

    const encoding = asserts.encoding()
    await encoding.succeed(DateTime.makeUnsafe("2021-01-01T00:00:00.000Z"))
  })

  it("DateTimeUtcFromValidDate", async () => {
    const schema = Schema.DateTimeUtcFromDate
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed(new Date("2021-01-01T00:00:00.000Z"), DateTime.makeUnsafe("2021-01-01T00:00:00.000Z"))
    await decoding.fail(new Date("invalid date"), `Expected a valid date, got Invalid Date`)

    const encoding = asserts.encoding()
    await encoding.succeed(DateTime.makeUnsafe("2021-01-01T00:00:00.000Z"), new Date("2021-01-01T00:00:00.000Z"))
  })

  it("DateTimeUtcFromString", async () => {
    const schema = Schema.DateTimeUtcFromString
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed("2021-01-01T00:00:00.000Z", DateTime.makeUnsafe("2021-01-01T00:00:00.000Z"))
    await decoding.fail("invalid", `Invalid UTC DateTime string: invalid`)
    await decoding.fail(null, `Expected string, got null`)

    const encoding = asserts.encoding()
    await encoding.succeed(DateTime.makeUnsafe("2021-01-01T00:00:00.000Z"), "2021-01-01T00:00:00.000Z")
  })

  it("DateTimeUtcFromMillis", async () => {
    const schema = Schema.DateTimeUtcFromMillis
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed(1609459200000, DateTime.makeUnsafe("2021-01-01T00:00:00.000Z"))
    await decoding.fail(null, `Expected number, got null`)

    const encoding = asserts.encoding()
    await encoding.succeed(DateTime.makeUnsafe("2021-01-01T00:00:00.000Z"), 1609459200000)
  })

  it("TimeZoneOffset", async () => {
    const schema = Schema.TimeZoneOffset
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000))

    const encoding = asserts.encoding()
    await encoding.succeed(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000))
  })

  it("TimeZoneNamed", async () => {
    const schema = Schema.TimeZoneNamed
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed(DateTime.zoneMakeNamedUnsafe("Europe/London"))

    const encoding = asserts.encoding()
    await encoding.succeed(DateTime.zoneMakeNamedUnsafe("Europe/London"))
  })

  it("TimeZone", async () => {
    const schema = Schema.TimeZone
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000))
    await decoding.succeed(DateTime.zoneMakeNamedUnsafe("Europe/London"))

    const encoding = asserts.encoding()
    await encoding.succeed(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000))
    await encoding.succeed(DateTime.zoneMakeNamedUnsafe("Europe/London"))
  })

  it("DateTimeZoned", async () => {
    const schema = Schema.DateTimeZoned
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed(DateTime.makeZonedUnsafe("2021-01-01T00:00:00.000Z", { timeZone: "Europe/London" }))

    const encoding = asserts.encoding()
    await encoding.succeed(DateTime.makeZonedUnsafe("2021-01-01T00:00:00.000Z", { timeZone: "Europe/London" }))
  })

  it("ReadonlySet", async () => {
    const schema = Schema.ReadonlySet(Schema.FiniteFromString)
    const asserts = new TestSchema.Asserts(schema)

    strictEqual(schema.value, Schema.FiniteFromString)
    strictEqual(schema.annotate({}).value, Schema.FiniteFromString)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed(new Set(["1", "2", "3"]), new Set([1, 2, 3]))
    await decoding.fail(null, `Expected ReadonlySet, got null`)
    await decoding.fail(
      new Set(["1", "2", null]),
      `Expected string, got null
  at ["values"][2]`
    )
  })

  it("HashSet", async () => {
    const schema = Schema.HashSet(Schema.FiniteFromString)
    const asserts = new TestSchema.Asserts(schema)

    strictEqual(schema.value, Schema.FiniteFromString)
    strictEqual(schema.annotate({}).value, Schema.FiniteFromString)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed(HashSet.make("1", "2", "3"), HashSet.make(1, 2, 3))
    await decoding.fail(null, `Expected HashSet, got null`)
    await decoding.fail(
      HashSet.make(null),
      `Expected string, got null
  at ["values"][0]`
    )

    const encoding = asserts.encoding()
    await encoding.succeed(HashSet.make(1, 2, 3), HashSet.make("1", "2", "3"))
  })

  it("Chunk", async () => {
    const schema = Schema.Chunk(Schema.FiniteFromString)
    const asserts = new TestSchema.Asserts(schema)

    strictEqual(schema.value, Schema.FiniteFromString)
    strictEqual(schema.annotate({}).value, Schema.FiniteFromString)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed(Chunk.make("1", "2", "3"), Chunk.make(1, 2, 3))
    await decoding.fail(null, `Expected Chunk, got null`)
    await decoding.fail(
      Chunk.make(null),
      `Expected string, got null
  at ["values"][0]`
    )

    const encoding = asserts.encoding()
    await encoding.succeed(Chunk.make(1, 2, 3), Chunk.make("1", "2", "3"))
  })

  it("ReadonlyMap", async () => {
    const schema = Schema.ReadonlyMap(Schema.String, Schema.FiniteFromString)
    const asserts = new TestSchema.Asserts(schema)

    strictEqual(schema.key, Schema.String)
    strictEqual(schema.annotate({}).key, Schema.String)
    strictEqual(schema.value, Schema.FiniteFromString)
    strictEqual(schema.annotate({}).value, Schema.FiniteFromString)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed(new Map([["a", "1"]]), new Map([["a", 1]]))
    await decoding.fail(null, `Expected ReadonlyMap, got null`)
    await decoding.fail(
      new Map([["a", null]]),
      `Expected string, got null
  at ["entries"][0][1]`
    )

    const encoding = asserts.encoding()
    await encoding.succeed(new Map([["a", 1]]), new Map([["a", "1"]]))
  })

  it("HashMap", async () => {
    const schema = Schema.HashMap(Schema.String, Schema.FiniteFromString)
    const asserts = new TestSchema.Asserts(schema)

    strictEqual(schema.key, Schema.String)
    strictEqual(schema.annotate({}).key, Schema.String)
    strictEqual(schema.value, Schema.FiniteFromString)
    strictEqual(schema.annotate({}).value, Schema.FiniteFromString)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed(HashMap.make(["a", "1"]), HashMap.make(["a", 1]))
    await decoding.fail(null, `Expected HashMap, got null`)
    await decoding.fail(
      HashMap.make(["a", null]),
      `Expected string, got null
  at ["entries"][0][1]`
    )

    const encoding = asserts.encoding()
    await encoding.succeed(HashMap.make(["a", 1]), HashMap.make(["a", "1"]))
  })

  describe("Transformations", () => {
    it("toLowerCase", async () => {
      const schema = Schema.String.pipe(
        Schema.decodeTo(
          Schema.String,
          SchemaTransformation.toLowerCase()
        )
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("A", "a")
      await decoding.succeed("B", "b")
    })

    it("toUpperCase", async () => {
      const schema = Schema.String.pipe(
        Schema.decodeTo(Schema.String, SchemaTransformation.toUpperCase())
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a", "A")
      await decoding.succeed("b", "B")
    })
  })

  describe("Opaque", () => {
    it("Struct", () => {
      class A extends Schema.Opaque<A>()(Schema.Struct({ a: Schema.String })) {}

      const schema = A

      const instance = schema.make({ a: "a" })
      strictEqual(instance.a, "a")
      deepStrictEqual(A.fields, { a: Schema.String })
    })
  })

  describe("instanceOf", () => {
    it("arg: message: string", async () => {
      class MyError extends Error {
        constructor(message?: string) {
          super(message)
          this.name = "MyError"
          Object.setPrototypeOf(this, MyError.prototype)
        }
      }

      const schema = Schema.instanceOf(
        MyError,
        { expected: "MyError" }
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(new MyError("a"))
      await decoding.fail(null, `Expected MyError, got null`)

      const encoding = asserts.encoding()
      await encoding.succeed(new MyError("a"))
      await encoding.fail(null, `Expected MyError, got null`)
    })
  })

  it("Duration", async () => {
    const schema = Schema.Duration
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }
  })

  it("DurationFromString", async () => {
    const schema = Schema.DurationFromString
    const asserts = new TestSchema.Asserts(schema)

    const decoding = asserts.decoding()
    await decoding.succeed("1000 millis", Duration.millis(1000))
    await decoding.succeed("1 second", Duration.seconds(1))
    await decoding.succeed("Infinity", Duration.infinity)
    await decoding.succeed("-Infinity", Duration.negativeInfinity)
    await decoding.fail("value", "Invalid Duration string: value")

    const encoding = asserts.encoding()
    await encoding.succeed(Duration.zero, "0 millis")
    await encoding.succeed(Duration.seconds(5), "5000 millis")
    await encoding.succeed(Duration.nanos(5000n), "5000 nanos")
    await encoding.succeed(Duration.infinity, "Infinity")
    await encoding.succeed(Duration.negativeInfinity, "-Infinity")
  })

  it("DurationFromNanos", async () => {
    const schema = Schema.DurationFromNanos
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed(0n, Duration.zero)
    await decoding.succeed(1000n, Duration.nanos(1000n))

    const encoding = asserts.encoding()
    await encoding.succeed(Duration.millis(5), 5_000_000n)
    await encoding.succeed(Duration.nanos(5000n), 5000n)
    await encoding.fail(Duration.infinity, "Unable to encode Infinity into a bigint")
  })

  it("DurationFromMillis", async () => {
    const schema = Schema.DurationFromMillis
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed(Infinity, Duration.infinity)
    await decoding.succeed(0, Duration.millis(0))
    await decoding.succeed(1000, Duration.seconds(1))
    await decoding.succeed(60 * 1000, Duration.minutes(1))
    await decoding.succeed(0.1, Duration.millis(0.1))
    await decoding.fail(-1, "Expected a value greater than or equal to 0, got -1")
    await decoding.fail(NaN, "Expected a value greater than or equal to 0, got NaN")

    const encoding = asserts.encoding()
    await encoding.succeed(Duration.infinity, Infinity)
    await encoding.succeed(Duration.millis(NaN), 0)
    await encoding.succeed(Duration.seconds(5), 5000)
    await encoding.succeed(Duration.millis(5000), 5000)
    await encoding.succeed(Duration.millis(0.1), 0.1)
    await encoding.succeed(Duration.nanos(5000n), 0.005)
  })

  it("BigDecimal", async () => {
    const schema = Schema.BigDecimal
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed(BigDecimal.fromStringUnsafe("123.45"))
    await decoding.fail(null, `Expected BigDecimal, got null`)

    const encoding = asserts.encoding()
    await encoding.succeed(BigDecimal.fromStringUnsafe("123.45"))
  })

  describe("BigDecimal checks", () => {
    it("isGreaterThanBigDecimal", async () => {
      const schema = Schema.BigDecimal.check(Schema.isGreaterThanBigDecimal(BigDecimal.fromStringUnsafe("1")))
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(BigDecimal.fromStringUnsafe("2"))
      await decoding.fail(
        BigDecimal.fromStringUnsafe("1"),
        `Expected a value greater than 1, got BigDecimal(1)`
      )
    })

    it("isGreaterThanOrEqualToBigDecimal", async () => {
      const schema = Schema.BigDecimal.check(
        Schema.isGreaterThanOrEqualToBigDecimal(BigDecimal.fromStringUnsafe("1"))
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(BigDecimal.fromStringUnsafe("1"))
      await decoding.fail(
        BigDecimal.fromStringUnsafe("0"),
        `Expected a value greater than or equal to 1, got BigDecimal(0)`
      )
    })

    it("isLessThanBigDecimal", async () => {
      const schema = Schema.BigDecimal.check(Schema.isLessThanBigDecimal(BigDecimal.fromStringUnsafe("1")))
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(BigDecimal.fromStringUnsafe("0"))
      await decoding.fail(
        BigDecimal.fromStringUnsafe("1"),
        `Expected a value less than 1, got BigDecimal(1)`
      )
    })

    it("isLessThanOrEqualToBigDecimal", async () => {
      const schema = Schema.BigDecimal.check(Schema.isLessThanOrEqualToBigDecimal(BigDecimal.fromStringUnsafe("1")))
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(BigDecimal.fromStringUnsafe("1"))
      await decoding.fail(
        BigDecimal.fromStringUnsafe("2"),
        `Expected a value less than or equal to 1, got BigDecimal(2)`
      )
    })

    it("isBetweenBigDecimal", async () => {
      const schema = Schema.BigDecimal.check(Schema.isBetweenBigDecimal({
        minimum: BigDecimal.fromStringUnsafe("1"),
        maximum: BigDecimal.fromStringUnsafe("5")
      }))
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(BigDecimal.fromStringUnsafe("3"))
      await decoding.fail(
        BigDecimal.fromStringUnsafe("0"),
        `Expected a value between 1 and 5, got BigDecimal(0)`
      )
    })
  })

  describe("tag", () => {
    it("decoding: required & encoding: required & constructor: required", async () => {
      const schema = Schema.Struct({
        _tag: Schema.Literal("a"),
        a: Schema.FiniteFromString
      })
      const asserts = new TestSchema.Asserts(schema)

      const make = asserts.make()
      await make.succeed({ _tag: "a", a: 1 })

      const decoding = asserts.decoding()
      await decoding.succeed({ _tag: "a", a: "1" }, { _tag: "a", a: 1 })

      const encoding = asserts.encoding()
      await encoding.succeed({ _tag: "a", a: 1 }, { _tag: "a", a: "1" })
    })

    it("decoding: required & encoding: required & constructor: optional", async () => {
      const schema = Schema.Struct({
        _tag: Schema.tag("a"),
        a: Schema.FiniteFromString
      })
      const asserts = new TestSchema.Asserts(schema)

      const make = asserts.make()
      await make.succeed({ _tag: "a", a: 1 })
      await make.succeed({ a: 1 }, { _tag: "a", a: 1 })

      const decoding = asserts.decoding()
      await decoding.succeed({ _tag: "a", a: "1" }, { _tag: "a", a: 1 })

      const encoding = asserts.encoding()
      await encoding.succeed({ _tag: "a", a: 1 }, { _tag: "a", a: "1" })
    })

    it("decoding: default & encoding: omit & constructor: optional", async () => {
      const schema = Schema.Struct({
        _tag: Schema.tag("a").pipe(
          Schema.encodeTo(
            Schema.optionalKey(Schema.Literal("a")),
            {
              decode: SchemaGetter.withDefault(Effect.succeed("a")),
              encode: SchemaGetter.omit()
            }
          )
        ),
        a: Schema.FiniteFromString
      })
      const asserts = new TestSchema.Asserts(schema)

      const make = asserts.make()
      await make.succeed({ _tag: "a", a: 1 })
      await make.succeed({ a: 1 }, { _tag: "a", a: 1 })

      const decoding = asserts.decoding()
      await decoding.succeed({ _tag: "a", a: "1" }, { _tag: "a", a: 1 })
      await decoding.succeed({ a: "1" }, { _tag: "a", a: 1 })

      const encoding = asserts.encoding()
      await encoding.succeed({ _tag: "a", a: 1 }, { a: "1" })
    })
  })

  it("tagDefaultOmit", async () => {
    const schema = Schema.Struct({
      _tag: Schema.tagDefaultOmit("a"),
      a: Schema.FiniteFromString
    })
    const asserts = new TestSchema.Asserts(schema)

    const make = asserts.make()
    await make.succeed({ _tag: "a", a: 1 })
    await make.succeed({ a: 1 }, { _tag: "a", a: 1 })
    await make.fail(
      { _tag: "c", a: 1 },
      `Expected "a", got "c"
  at ["_tag"]`
    )

    const decoding = asserts.decoding()
    await decoding.succeed({ _tag: "a", a: "1" }, { _tag: "a", a: 1 })
    await decoding.succeed({ a: "1" }, { _tag: "a", a: 1 })
    await decoding.fail(
      { _tag: "c", a: 1 },
      `Expected "a", got "c"
  at ["_tag"]`
    )

    const encoding = asserts.encoding()
    await encoding.succeed({ _tag: "a", a: 1 }, { a: "1" })
    await encoding.succeed({ a: 1 }, { a: "1" })
  })

  it("URL", async () => {
    const schema = Schema.URL
    const asserts = new TestSchema.Asserts(schema)
    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }
  })

  it("RegExp", async () => {
    const schema = Schema.RegExp
    const asserts = new TestSchema.Asserts(schema)
    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }
  })

  it("URLFromString", async () => {
    const schema = Schema.URLFromString
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed("https://effect.website", new URL("https://effect.website"))
    await decoding.fail(
      "123",
      `Invalid URL string: 123`
    )

    const encoding = asserts.encoding()
    await encoding.succeed(new URL("https://effect.website"), "https://effect.website/")
  })

  describe("UnknownFromJsonString / fromJsonString", () => {
    it("use case: Unknown <-> JSON string", async () => {
      const schema = Schema.UnknownFromJsonString
      const asserts = new TestSchema.Asserts(schema)

      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const decoding = asserts.decoding()
      await decoding.succeed(`{"a":1}`, { a: 1 })
      await decoding.fail(
        `{"a"`,
        "SyntaxError: Expected ':' after property name in JSON at position 4 (line 1 column 5)"
      )

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1 }, `{"a":1}`)
    })

    it("use case: create a JSON string serializer for an existing schema", async () => {
      const schema = Schema.fromJsonString(Schema.Struct({ b: Schema.Number }))
      const asserts = new TestSchema.Asserts(schema)

      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const decoding = asserts.decoding()
      await decoding.succeed(`{"b":1}`, { b: 1 })
      await decoding.fail(
        `{"a":null}`,
        `Missing key
  at ["b"]`
      )
    })

    it("use case: parse / stringify a nested schema", async () => {
      const schema = Schema.Struct({
        a: Schema.fromJsonString(Schema.Struct({ b: Schema.Number }))
      })
      const asserts = new TestSchema.Asserts(schema)

      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const decoding = asserts.decoding()
      await decoding.succeed({ a: `{"b":2}` }, { a: { b: 2 } })
      await decoding.fail(
        { a: `{"a":null}` },
        `Missing key
  at ["a"]["b"]`
      )
    })
  })

  it("fromFormData", async () => {
    const schema = Schema.fromFormData(Schema.Struct({
      a: Schema.NonEmptyString,
      b: Schema.Struct({
        c: Schema.String,
        d: Schema.String
      }),
      e: Schema.Array(Schema.String)
    }))
    const asserts = new TestSchema.Asserts(schema)

    const formData = new FormData()
    formData.append("a", "a")
    formData.append("b[c]", "bc")
    formData.append("b[d]", "bd")
    formData.append("e[0]", "e0")
    formData.append("e[1]", "e1")

    const decoded = { a: "a", b: { c: "bc", d: "bd" }, e: ["e0", "e1"] }

    const decoding = asserts.decoding()
    await decoding.succeed(formData, decoded)

    const encoding = asserts.encoding()

    {
      const formData = new FormData()
      formData.append("a", "a")
      formData.append("b[c]", "bc")
      formData.append("b[d]", "bd")
      formData.append("e", "e0")
      formData.append("e", "e1")
      await encoding.succeed(decoded, formData)
    }

    {
      const formData = new FormData()
      formData.append("a", "")
      await decoding.fail(
        formData,
        `Expected a value with a length of at least 1, got ""
  at ["a"]`
      )
    }
  })

  it("fromURLSearchParams", async () => {
    const schema = Schema.fromURLSearchParams(Schema.Struct({
      a: Schema.NonEmptyString,
      b: Schema.Struct({
        c: Schema.String,
        d: Schema.String
      }),
      e: Schema.Array(Schema.String)
    }))
    const asserts = new TestSchema.Asserts(schema)

    const urlSearchParams = new URLSearchParams("a=a&b[c]=bc&b[d]=bd&e=e0&e=e1")

    const decoded = { a: "a", b: { c: "bc", d: "bd" }, e: ["e0", "e1"] }

    const decoding = asserts.decoding()
    await decoding.succeed(urlSearchParams, decoded)

    const encoding = asserts.encoding()

    {
      const urlSearchParams = new URLSearchParams("a=a&b[c]=bc&b[d]=bd&e=e0&e=e1")
      await encoding.succeed(decoded, urlSearchParams)
    }

    {
      const urlSearchParams = new URLSearchParams("a=")
      await decoding.fail(
        urlSearchParams,
        `Expected a value with a length of at least 1, got ""
  at ["a"]`
      )
    }
  })

  it("Trim", async () => {
    const schema = Schema.Trim
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed("a")
    await decoding.succeed("a ", "a")
    await decoding.succeed(" a", "a")
    await decoding.succeed(" a ", "a")
    await decoding.succeed("a\n", "a")

    const encoding = asserts.encoding()
    await encoding.succeed("a")
    await encoding.fail(
      "a ",
      `Expected a string with no leading or trailing whitespace, got "a "`
    )
  })

  it("transformOrFail", async () => {
    const schema = Schema.String.pipe(
      Schema.decodeTo(
        Schema.String,
        SchemaTransformation.transformOrFail({
          decode: (s) =>
            s === "a"
              ? Effect.fail(new SchemaIssue.Forbidden(Option.some(s), { message: `input should not be "a"` }))
              : Effect.succeed(s),
          encode: (s) =>
            s === "b"
              ? Effect.fail(new SchemaIssue.Forbidden(Option.some(s), { message: `input should not be "b"` }))
              : Effect.succeed(s)
        })
      )
    )
    const asserts = new TestSchema.Asserts(schema)

    const decoding = asserts.decoding()
    await decoding.succeed("b")
    await decoding.fail(
      "a",
      `input should not be "a"`
    )

    const encoding = asserts.encoding()
    await encoding.succeed("a")
    await encoding.fail(
      "b",
      `input should not be "b"`
    )
  })

  describe("TemplateLiteral", () => {
    it("should expose the parts", () => {
      const parts = ["a", Schema.String] as const
      const schema = Schema.TemplateLiteral(parts)
      deepStrictEqual(schema.parts, parts)
    })

    it(`NonEmptyString + String`, async () => {
      const schema = Schema.TemplateLiteral([Schema.NonEmptyString, Schema.String])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a")
    })

    it("rejects checks on Literal, TemplateLiteral, and Union parts", () => {
      const check = Schema.makeFilter(() => true)

      throws(
        () => Schema.TemplateLiteral([Schema.Literal("a").check(check)]),
        "Invalid TemplateLiteral part Literal"
      )
      throws(
        () => Schema.TemplateLiteral([Schema.TemplateLiteral(["a"]).check(check)]),
        "Invalid TemplateLiteral part TemplateLiteral"
      )
      throws(
        () => Schema.TemplateLiteral([Schema.Union([Schema.Literal("a"), Schema.Literal("b")]).check(check)]),
        "Invalid TemplateLiteral part Union"
      )
    })

    it(`"a"`, async () => {
      const schema = Schema.TemplateLiteral(["a"])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a")
      await decoding.fail(null, "Expected string, got null")
      await decoding.fail(
        "ab",
        `Expected a string matching template literal parts, got "ab"`
      )
      await decoding.fail(
        "",
        `Expected a string matching template literal parts, got ""`
      )
    })

    it(`"a b"`, async () => {
      const schema = Schema.TemplateLiteral(["a", " ", "b"])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a b")

      await decoding.fail(
        "a  b",
        `Expected a string matching template literal parts, got "a  b"`
      )
    })

    it(`"[" + string + "]"`, async () => {
      const schema = Schema.TemplateLiteral(["[", Schema.String, "]"])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("[a]")

      await decoding.fail(
        "a",
        `Expected a string matching template literal parts, got "a"`
      )
    })

    it(`"a" + string`, async () => {
      const schema = Schema.TemplateLiteral(["a", Schema.String])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a")
      await decoding.succeed("ab")

      await decoding.fail(
        null,
        "Expected string, got null"
      )
      await decoding.fail(
        "",
        `Expected a string matching template literal parts, got ""`
      )
    })

    it(`"a" + number`, async () => {
      const schema = Schema.TemplateLiteral(["a", Schema.Number])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a1")
      await decoding.succeed("a+1")
      await decoding.succeed("a1.2")

      await decoding.succeed("a-1.401298464324817e-45")
      await decoding.succeed("a1.401298464324817e-45")
      await decoding.succeed("a+1.401298464324817e-45")
      await decoding.succeed("a-1.401298464324817e+45")
      await decoding.succeed("a1.401298464324817e+45")
      await decoding.succeed("a+1.401298464324817e+45")

      await decoding.succeed("a-1.401298464324817E-45")
      await decoding.succeed("a1.401298464324817E-45")
      await decoding.succeed("a+1.401298464324817E-45")
      await decoding.succeed("a-1.401298464324817E+45")
      await decoding.succeed("a1.401298464324817E+45")
      await decoding.succeed("a+1.401298464324817E+45")

      await decoding.fail(
        null,
        "Expected string, got null"
      )
      await decoding.fail(
        "",
        `Expected a string matching template literal parts, got ""`
      )
      await decoding.fail(
        "aa",
        `Expected a string matching template literal parts, got "aa"`
      )
    })

    it(`"a" + bigint`, async () => {
      const schema = Schema.TemplateLiteral(["a", Schema.BigInt])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a0")
      await decoding.succeed("a1")
      await decoding.succeed("a-1")

      await decoding.fail(
        null,
        "Expected string, got null"
      )
      await decoding.fail(
        "",
        `Expected a string matching template literal parts, got ""`
      )
      await decoding.fail(
        "aa",
        `Expected a string matching template literal parts, got "aa"`
      )
      await decoding.fail(
        "a1.2",
        `Expected a string matching template literal parts, got "a1.2"`
      )
      await decoding.fail(
        "a+1",
        `Expected a string matching template literal parts, got "a+1"`
      )
    })

    it(`string`, async () => {
      const schema = Schema.TemplateLiteral([Schema.String])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a")
      await decoding.succeed("ab")
      await decoding.succeed("")
      await decoding.succeed("\n")
      await decoding.succeed("\r")
      await decoding.succeed("\r\n")
      await decoding.succeed("\t")
    })

    it(`\\n + string`, async () => {
      const schema = Schema.TemplateLiteral(["\n", Schema.String])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("\n")
      await decoding.succeed("\na")
      await decoding.fail(
        "a",
        `Expected a string matching template literal parts, got "a"`
      )
    })

    it(`a\\nb  + string`, async () => {
      const schema = Schema.TemplateLiteral(["a\nb ", Schema.String])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a\nb ")
      await decoding.succeed("a\nb c")
    })

    it(`"a" + string + "b"`, async () => {
      const schema = Schema.TemplateLiteral(["a", Schema.String, "b"])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("ab")
      await decoding.succeed("acb")
      await decoding.succeed("abb")
      await decoding.fail(
        "",
        `Expected a string matching template literal parts, got ""`
      )
      await decoding.fail(
        "a",
        `Expected a string matching template literal parts, got "a"`
      )
      await decoding.fail(
        "b",
        `Expected a string matching template literal parts, got "b"`
      )

      const encoding = asserts.encoding()
      await encoding.succeed("acb")
    })

    it(`"a" + string + "b" + string`, async () => {
      const schema = Schema.TemplateLiteral(["a", Schema.String, "b", Schema.String])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("ab")
      await decoding.succeed("acb")
      await decoding.succeed("acbd")
      await decoding.fail(
        "a",
        `Expected a string matching template literal parts, got "a"`
      )
      await decoding.fail(
        "b",
        `Expected a string matching template literal parts, got "b"`
      )
    })

    it("https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html", async () => {
      const EmailLocaleIDs = Schema.Literals(["welcome_email", "email_heading"])
      const FooterLocaleIDs = Schema.Literals(["footer_title", "footer_sendoff"])
      const schema = Schema.TemplateLiteral([Schema.Union([EmailLocaleIDs, FooterLocaleIDs]), "_id"])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("welcome_email_id")
      await decoding.succeed("email_heading_id")
      await decoding.succeed("footer_title_id")
      await decoding.succeed("footer_sendoff_id")

      await decoding.fail(
        "_id",
        `Expected a string matching template literal parts, got "_id"`
      )
    })

    it(`string + 0`, async () => {
      const schema = Schema.TemplateLiteral([Schema.String, 0])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a0")
      await decoding.fail(
        "a",
        `Expected a string matching template literal parts, got "a"`
      )
    })

    it(`string + 1n`, async () => {
      const schema = Schema.TemplateLiteral([Schema.String, 1n])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a1")
      await decoding.fail(
        "a",
        `Expected a string matching template literal parts, got "a"`
      )
    })

    it(`string + ("a" | 0)`, async () => {
      const schema = Schema.TemplateLiteral([Schema.String, Schema.Literals(["a", 0])])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a0")
      await decoding.succeed("aa")
      await decoding.fail(
        "b",
        `Expected a string matching template literal parts, got "b"`
      )
    })

    it(`(string | 1) + (number | true)`, async () => {
      const schema = Schema.TemplateLiteral([
        Schema.Union([Schema.String, Schema.Literal(1)]),
        Schema.Union([Schema.Number, Schema.Literal("true")])
      ])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("atrue")
      await decoding.succeed("-2")
      await decoding.succeed("10.1")
      await decoding.fail(
        "",
        `Expected a string matching template literal parts, got ""`
      )
    })

    it("`c${`a${string}b` | \"e\"}d`", async () => {
      const schema = Schema.TemplateLiteral(
        ["c", Schema.Union([Schema.TemplateLiteral(["a", Schema.String, "b"]), Schema.Literal("e")]), "d"]
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("ced")
      await decoding.succeed("cabd")
      await decoding.succeed("casbd")
      await decoding.succeed("ca  bd")
      await decoding.fail(
        "",
        `Expected a string matching template literal parts, got ""`
      )
    })

    it("< + h + (1|2n) + >", async () => {
      const schema = Schema.TemplateLiteral(["<", Schema.TemplateLiteral(["h", Schema.Literals([1, 2n])]), ">"])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("<h1>")
      await decoding.succeed("<h2>")
      await decoding.fail(
        "<h3>",
        `Expected a string matching template literal parts, got "<h3>"`
      )
    })

    it(`"a" + check`, async () => {
      const schema = Schema.TemplateLiteral(["a", Schema.NonEmptyString])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("ab")
      await decoding.fail(
        null,
        "Expected string, got null"
      )
      await decoding.fail(
        "",
        `Expected a string matching template literal parts, got ""`
      )
      await decoding.fail(
        "a",
        `Expected a string matching template literal parts, got "a"`
      )
    })

    it(`"a" + transformation`, async () => {
      const schema = Schema.TemplateLiteral(["a", Schema.FiniteFromString])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a")
      await decoding.succeed("a1")

      await decoding.fail(
        null,
        "Expected string, got null"
      )
      await decoding.fail(
        "",
        `Expected a string matching template literal parts, got ""`
      )
      await decoding.fail(
        "ab",
        `Expected a finite number, got NaN
  at [1]`
      )
    })
  })

  describe("TemplateLiteralParser", () => {
    it("should expose the parts", () => {
      const parts = ["a", Schema.String] as const
      const schema = Schema.TemplateLiteralParser(parts)
      deepStrictEqual(schema.parts, parts)
    })

    it(`NonEmptyString + String`, async () => {
      const schema = Schema.TemplateLiteralParser([Schema.NonEmptyString, Schema.String])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a", ["a", ""])
    })

    it(`"a"`, async () => {
      const schema = Schema.TemplateLiteralParser(["a"])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a", ["a"])
      await decoding.fail(
        "ab",
        `Expected a string matching template literal parts, got "ab"`
      )
      await decoding.fail(
        "",
        `Expected a string matching template literal parts, got ""`
      )
      await decoding.fail(
        null,
        "Expected string, got null"
      )
    })

    it(`"a b"`, async () => {
      const schema = Schema.TemplateLiteralParser(["a", " ", "b"])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a b", ["a", " ", "b"])

      await decoding.fail(
        "a  b",
        `Expected a string matching template literal parts, got "a  b"`
      )
    })

    it(`Int + "a"`, async () => {
      const schema = Schema.TemplateLiteralParser([Schema.Int, "a"])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("1a", [1, "a"])
      await decoding.fail(
        "1.1a",
        `Expected a string matching template literal parts, got "1.1a"`
      )

      const encoding = asserts.encoding()
      await encoding.succeed([1, "a"], "1a")
      await encoding.fail(
        [1.1, "a"],
        `Expected an integer, got 1.1
  at [0]`
      )
    })

    it(`Int + String`, async () => {
      const schema = Schema.TemplateLiteralParser([Schema.Number.check(Schema.isInt()), Schema.String])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("1.2", [1, ".2"])
    })

    it(`NumberFromString + "a" + NonEmptyString`, async () => {
      const schema = Schema.TemplateLiteralParser([Schema.FiniteFromString, "a", Schema.NonEmptyString])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("100ab", [100, "a", "b"])
      await decoding.succeed("100ab23a", [100, "a", "b23a"])
      await decoding.fail(
        "-ab",
        `Expected a finite number, got NaN
  at [0]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed([100, "a", "b"], "100ab")
      await encoding.fail(
        [100, "a", ""],
        `Expected a value with a length of at least 1, got ""
  at [2]`
      )
    })

    it(`"h" + (1 | 2 | 3)`, async () => {
      const schema = Schema.TemplateLiteralParser(["h", Schema.Literals([1, 2, 3])])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("h1", ["h", 1])
    })

    it(`"c" + (\`a\${string}b\`|"e") + "d"`, async () => {
      const schema = Schema.TemplateLiteralParser([
        "c",
        Schema.Union([Schema.TemplateLiteralParser(["a", Schema.NonEmptyString, "b"]), Schema.Literal("e")]),
        "d"
      ])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("ca bd", ["c", ["a", " ", "b"], "d"])
      await decoding.succeed("ced", ["c", "e", "d"])
      await decoding.fail(
        "cabd",
        `Expected a string matching template literal parts, got "ab"
  at [1]`
      )
      await decoding.fail(
        "ed",
        `Expected a string matching template literal parts, got "ed"`
      )
    })

    it(`"c" + (\`a\${number}b\`|"e") + "d"`, async () => {
      const schema = Schema.TemplateLiteralParser([
        "c",
        Schema.Union([
          Schema.TemplateLiteralParser(["a", Schema.Finite.check(Schema.isInt()), "b"]),
          Schema.Literal("e")
        ]),
        "d"
      ])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("ced", ["c", "e", "d"])
      await decoding.succeed("ca1bd", ["c", ["a", 1, "b"], "d"])
      await decoding.fail(
        "ca1.1bd",
        `Expected a string matching template literal parts, got "a1.1b"
  at [1]`
      )
      await decoding.fail(
        "ca-bd",
        `Expected a string matching template literal parts, got "a-b"
  at [1]`
      )
    })

    it(`readonly ["<", \`h\${1 | 2}\`, ">"]`, async () => {
      const schema = Schema.TemplateLiteralParser(["<", Schema.TemplateLiteral(["h", Schema.Literals([1, 2])]), ">"])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("<h1>", ["<", "h1", ">"])
      await decoding.succeed("<h2>", ["<", "h2", ">"])
      await decoding.fail(
        "<h3>",
        `Expected a string matching template literal parts, got "<h3>"`
      )
    })

    it(`readonly ["<", readonly ["h", 1 | 2], ">"]`, async () => {
      const schema = Schema.TemplateLiteralParser([
        "<",
        Schema.TemplateLiteralParser(["h", Schema.Literals([1, 2])]),
        ">"
      ])
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("<h1>", ["<", ["h", 1], ">"])
      await decoding.succeed("<h2>", ["<", ["h", 2], ">"])
      await decoding.fail(
        "<h3>",
        `Expected a string matching template literal parts, got "h3"
  at [1]`
      )
    })
  })

  describe("Class", () => {
    it("make with void input", () => {
      class A extends Schema.Class<A>("A")({}) {}
      deepStrictEqual(A.make(), new A())
      deepStrictEqual(A.makeOption(), Option.some(new A()))
      deepStrictEqual(Effect.runSync(A.makeEffect()), new A())
    })

    it("suspend before initialization", async () => {
      const schema = Schema.suspend(() => string)
      class A extends Schema.Class<A>("A")(Schema.Struct({ a: schema })) {}
      const string = Schema.String

      const asserts = new TestSchema.Asserts(A)
      const make = asserts.make()
      await make.succeed(new A({ a: "a" }))
      await make.succeed({ a: "a" }, new A({ a: "a" }))

      const decoding = asserts.decoding()
      await decoding.succeed(new A({ a: "a" }))
    })

    it("should memoize the ast", () => {
      class A extends Schema.Class<A>("A")({
        a: Schema.String
      }) {}
      assertTrue(A.ast === A.ast)
    })

    it("should set the identifier annotation", () => {
      class A extends Schema.Class<A>("A")({
        a: Schema.String
      }) {}
      strictEqual(A.ast.annotations?.identifier, "A")
    })

    describe("should be compatible with `immer`", () => {
      it("`[immerable]`", () => {
        class A extends Schema.Class<A>("A")({
          a: Schema.Struct({ b: Schema.FiniteFromString }).pipe(Schema.optional),
          c: Schema.FiniteFromString
        }) {}

        const a = new A({ a: { b: 1 }, c: 2 })

        const modified = produce(a, (draft) => {
          if (draft.a) {
            draft.a.b = 2
          }
        })

        assertTrue(modified instanceof A)
        strictEqual(modified.a?.b, 2)
        strictEqual(modified.c, 2)
        strictEqual(a.a?.b, 1)
      })

      it("Equality", () => {
        class A extends Schema.Class<A>("A")({
          a: Schema.String
        }) {}

        const a = new A({ a: "a" })
        const a1 = produce(a, (draft) => {
          draft.a = "a1"
        })
        const a2 = produce(a, (draft) => {
          draft.a = "a1"
        })
        assertTrue(Equal.equals(a1, new A({ a: "a1" })))
        assertTrue(Equal.equals(a1, a2))
      })
    })

    it("mapFields", () => {
      class A extends Schema.Class<A>("A")({
        a: Schema.String
      }) {}
      const schema = A.mapFields((fields) => ({ ...fields, b: Schema.Number }))
      deepStrictEqual(schema.fields, { a: Schema.String, b: Schema.Number })
    })

    it("Struct with nested Class", async () => {
      class A extends Schema.Class<A, { readonly brand: unique symbol }>("A")(Schema.Struct({
        a: Schema.String
      })) {}
      const schema = Schema.Struct({
        a: A.pipe(Schema.withConstructorDefault(Effect.succeed(new A({ a: "default" }))))
      })
      const asserts = new TestSchema.Asserts(schema)

      const make = asserts.make()
      await make.succeed({ a: new A({ a: "a" }) })
      await make.succeed({}, { a: new A({ a: "default" }) })
    })

    it("Class with nested Class", async () => {
      class A extends Schema.Class<A, { readonly brand: unique symbol }>("A")(Schema.Struct({
        a: Schema.String
      })) {}
      class B extends Schema.Class<B, { readonly brand: unique symbol }>("B")(Schema.Struct({
        a: A.pipe(Schema.withConstructorDefault(Effect.succeed(new A({ a: "default" }))))
      })) {}
      const schema = B
      const asserts = new TestSchema.Asserts(schema)

      const make = asserts.make()
      await make.succeed({ a: new A({ a: "a" }) }, new B({ a: new A({ a: "a" }) }))
      await make.succeed({}, new B({ a: new A({ a: "default" }) }))
    })

    it("should be possible to define a class with a mutable field", async () => {
      class A extends Schema.Class<A>("A")({
        a: Schema.mutableKey(Schema.String)
      }) {
        public update() {
          this.a = "b"
        }
      }
      const asserts = new TestSchema.Asserts(A)

      const make = asserts.make()
      await make.succeed(new A({ a: "a" }))
      await make.succeed({ a: "a" }, new A({ a: "a" }))

      const a = new A({ a: "a" })
      a.update()
      strictEqual(a.a, "b")
    })

    it("Fields argument", async () => {
      class A extends Schema.Class<A>("A")({
        a: Schema.String
      }) {
        readonly _a = 1
      }
      const asserts = new TestSchema.Asserts(A)

      // should be a schema
      assertTrue(Schema.isSchema(A))
      // should expose the fields
      deepStrictEqual(A.fields, { a: Schema.String })
      // should expose the identifier
      strictEqual(A.identifier, "A")

      strictEqual(A.name, "A")

      assertTrue(new A({ a: "a" }) instanceof A)
      assertTrue(A.make({ a: "a" }) instanceof A)

      // test additional fields
      strictEqual(new A({ a: "a" })._a, 1)
      strictEqual(A.make({ a: "a" })._a, 1)

      // test Equal.equals
      assertTrue(Equal.equals(new A({ a: "a" }), new A({ a: "a" })))
      assertFalse(Equal.equals(new A({ a: "a" }), new A({ a: "b" })))

      const make = asserts.make()
      await make.succeed(new A({ a: "a" }))
      await make.succeed({ a: "a" }, new A({ a: "a" }))

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "a" }, new A({ a: "a" }))
      await decoding.fail(
        null,
        `Expected object, got null`
      )
      await decoding.fail(
        { a: 1 },
        `Expected string, got 1
  at ["a"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed(new A({ a: "a" }), { a: "a" })
      await encoding.fail(
        null,
        "Expected A, got null"
      )
      await encoding.fail(
        { a: "a" },
        `Expected A, got {"a":"a"}`
      )
    })

    it("Struct argument", async () => {
      class A extends Schema.Class<A>("A")(Schema.Struct({
        a: Schema.String
      })) {
        readonly _a = 1
      }
      const asserts = new TestSchema.Asserts(A)

      // should be a schema
      assertTrue(Schema.isSchema(A))
      // should expose the fields
      deepStrictEqual(A.fields, { a: Schema.String })
      // should expose the identifier
      strictEqual(A.identifier, "A")

      strictEqual(A.name, "A")

      assertTrue(new A({ a: "a" }) instanceof A)
      assertTrue(A.make({ a: "a" }) instanceof A)

      // test additional fields
      strictEqual(new A({ a: "a" })._a, 1)
      strictEqual(A.make({ a: "a" })._a, 1)

      // test Equal.equals
      assertTrue(Equal.equals(new A({ a: "a" }), new A({ a: "a" })))
      assertFalse(Equal.equals(new A({ a: "a" }), new A({ a: "b" })))

      const make = asserts.make()
      await make.succeed(new A({ a: "a" }))
      await make.succeed({ a: "a" }, new A({ a: "a" }))

      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "a" }, new A({ a: "a" }))
      await decoding.fail(
        { a: 1 },
        `Expected string, got 1
  at ["a"]`
      )

      const encoding = asserts.encoding()
      await encoding.succeed(new A({ a: "a" }), { a: "a" })
      await encoding.fail(
        null,
        "Expected A, got null"
      )
      await encoding.fail(
        { a: "a" },
        `Expected A, got {"a":"a"}`
      )
    })

    it("constructor ignores excess properties by default", () => {
      class A extends Schema.Class<A>("A")({
        a: Schema.String
      }) {}

      const instance = new A({ a: "a", extra: "extra" } as any)

      strictEqual(instance.a, "a")
      assertFalse("extra" in instance)
    })

    it("constructor preserves excess properties when requested", () => {
      class A extends Schema.Class<A>("A")({
        a: Schema.String
      }) {}

      const instance = new A({ a: "a", extra: "extra" } as any, {
        parseOptions: { onExcessProperty: "preserve" }
      })

      strictEqual(instance.a, "a")
      strictEqual((instance as any).extra, "extra")
    })

    it("constructor rejects excess properties when requested", () => {
      class A extends Schema.Class<A>("A")({
        a: Schema.String
      }) {}

      throws(() =>
        new A({ a: "a", extra: "extra" } as any, {
          parseOptions: { onExcessProperty: "error" }
        })
      )
    })

    it("annotate", async () => {
      class A extends Schema.Class<A>("A")({
        a: Schema.String
      }) {}

      const Annotated = A.annotate({})

      // should be a schema
      assertTrue(Schema.isSchema(Annotated))
      // should expose the fields
      deepStrictEqual(Annotated.from.fields, { a: Schema.String })

      assertTrue(Annotated.make(new A({ a: "a" })) instanceof A)
    })

    describe("extend", () => {
      it("basic", async () => {
        class A extends Schema.Class<A>("A")(Schema.Struct({
          a: Schema.String
        })) {
          readonly _a = 1
        }
        class B extends A.extend<B>("B")({
          b: Schema.Number
        }) {
          readonly _b = 2
        }
        const asserts = new TestSchema.Asserts(B)

        const instance = new B({ a: "a", b: 2 })

        assertTrue(instance instanceof A)
        assertTrue(B.make({ a: "a", b: 2 }) instanceof A)
        assertTrue(instance instanceof B)
        assertTrue(B.make({ a: "a", b: 2 }) instanceof B)

        strictEqual(instance.a, "a")
        strictEqual(instance._a, 1)
        strictEqual(instance.b, 2)
        strictEqual(instance._b, 2)

        const make = asserts.make()
        await make.succeed(new B({ a: "a", b: 2 }))
        await make.succeed({ a: "a", b: 2 }, new B({ a: "a", b: 2 }))

        const decoding = asserts.decoding()
        await decoding.succeed({ a: "a", b: 2 }, new B({ a: "a", b: 2 }))
      })

      it("constructor preserves subclass fields while ignoring excess properties by default", () => {
        class A extends Schema.Class<A>("A")({
          a: Schema.String
        }) {}
        class B extends A.extend<B>("B")({
          b: Schema.Number
        }) {}

        const instance = new B({ a: "a", b: 2, extra: "extra" } as any)

        strictEqual(instance.a, "a")
        strictEqual(instance.b, 2)
        assertFalse("extra" in instance)
      })

      it("constructor preserves subclass fields and excess properties when requested", () => {
        class A extends Schema.Class<A>("A")({
          a: Schema.String
        }) {}
        class B extends A.extend<B>("B")({
          b: Schema.Number
        }) {}

        const instance = new B({ a: "a", b: 2, extra: "extra" } as any, {
          parseOptions: { onExcessProperty: "preserve" }
        })

        strictEqual(instance.a, "a")
        strictEqual(instance.b, 2)
        strictEqual((instance as any).extra, "extra")
      })

      it("constructor does not treat subclass fields as excess properties", () => {
        class A extends Schema.Class<A>("A")({
          a: Schema.String
        }) {}
        class B extends A.extend<B>("B")({
          b: Schema.Number
        }) {}

        const instance = new B({ a: "a", b: 2 }, {
          parseOptions: { onExcessProperty: "error" }
        })

        strictEqual(instance.a, "a")
        strictEqual(instance.b, 2)

        throws(() =>
          new B({ a: "a", b: 2, extra: "extra" } as any, {
            parseOptions: { onExcessProperty: "error" }
          })
        )
      })

      it("Struct argument", async () => {
        class A extends Schema.Class<A>("A")(
          Schema.Struct({
            a: Schema.Number
          }).check(Schema.makeFilter(({ a }) => a > 0, { expected: "positive a" }))
        ) {}
        class B extends A.extend<B>("B")(
          Schema.Struct({
            b: Schema.Number
          }).check(Schema.makeFilter(({ b }) => b > 0, { expected: "positive b" }))
        ) {}
        const asserts = new TestSchema.Asserts(B)

        const make = asserts.make()
        await make.succeed({ a: 1, b: 1 }, new B({ a: 1, b: 1 }))
        await make.fail({ a: 0, b: 1 }, `Expected positive a, got {"a":0,"b":1}`)
        await make.fail({ a: 1, b: 0 }, `Expected positive b, got {"a":1,"b":0}`)
      })

      it("static members", async () => {
        class A extends Schema.Class<A>("A")({
          a: Schema.String
        }) {
          static readonly aStatic = "value"
        }
        class B extends A.extend<B, typeof A>("B")({
          b: Schema.Number
        }) {}
        strictEqual(B.aStatic, "value")
      })
    })
  })

  describe("TaggedClass", () => {
    it("make with void input", () => {
      class A extends Schema.TaggedClass<A>()("A", {}) {}
      deepStrictEqual(A.make(), new A())
      deepStrictEqual(A.makeOption(), Option.some(new A()))
      deepStrictEqual(Effect.runSync(A.makeEffect()), new A())
    })

    it("explicit identifier", async () => {
      class A extends Schema.TaggedClass<A>("B")("A", {
        a: Schema.String
      }) {}
      strictEqual(A.identifier, "B")
    })

    it("fields argument", async () => {
      class A extends Schema.TaggedClass<A>()("A", {
        a: Schema.String
      }) {}
      const asserts = new TestSchema.Asserts(A)

      assertTrue(Schema.isSchema(A))
      deepStrictEqual(Object.keys(A.fields).sort(), ["_tag", "a"])
      strictEqual(A.identifier, "A")

      const instance = new A({ a: "a" })
      strictEqual(instance._tag, "A")
      strictEqual(instance.a, "a")
      assertTrue(instance instanceof A)

      const make = asserts.make()
      await make.succeed(new A({ a: "a" }))
      await make.succeed({ a: "a" }, new A({ a: "a" }))

      const decoding = asserts.decoding()
      await decoding.succeed({ _tag: "A", a: "a" }, new A({ a: "a" }))

      const encoding = asserts.encoding()
      await encoding.succeed(new A({ a: "a" }), { _tag: "A", a: "a" })
    })

    it("constructor ignores excess properties by default", () => {
      class A extends Schema.TaggedClass<A>()("A", {
        a: Schema.String
      }) {}

      const instance = new A({ a: "a", extra: "extra" } as any)

      strictEqual(instance._tag, "A")
      strictEqual(instance.a, "a")
      assertFalse("extra" in instance)
    })

    it("Struct argument", async () => {
      class A extends Schema.TaggedClass<A>()(
        "A",
        Schema.Struct({
          a: Schema.String
        }).check(Schema.makeFilter(({ a }) => a.length > 0, { expected: `"a" being longer than 0` }))
      ) {}

      const asserts = new TestSchema.Asserts(A)

      const make = asserts.make()
      await make.succeed({ a: "a" }, new A({ a: "a" }))
      await make.fail({ a: "" }, `Expected "a" being longer than 0, got {"_tag":"A","a":""}`)

      const decoding = asserts.decoding()
      await decoding.succeed({ _tag: "A", a: "a" }, new A({ a: "a" }))
      await decoding.fail(
        { a: "a" },
        `Missing key
  at ["_tag"]`
      )
      await decoding.fail({ _tag: "A", a: "" }, `Expected "a" being longer than 0, got {"_tag":"A","a":""}`)
    })

    it("extended constructor does not treat subclass fields as excess properties", () => {
      class A extends Schema.TaggedClass<A>()("A", {
        a: Schema.String
      }) {}
      class B extends A.extend<B>("B")({
        b: Schema.Number
      }) {}

      const instance = new B({ a: "a", b: 2, extra: "extra" } as any)

      strictEqual(instance._tag, "A")
      strictEqual(instance.a, "a")
      strictEqual(instance.b, 2)
      assertFalse("extra" in instance)

      const strictInstance = new B({ a: "a", b: 2 }, {
        parseOptions: { onExcessProperty: "error" }
      })

      strictEqual(strictInstance._tag, "A")
      strictEqual(strictInstance.a, "a")
      strictEqual(strictInstance.b, 2)

      throws(() =>
        new B({ a: "a", b: 2, extra: "extra" } as any, {
          parseOptions: { onExcessProperty: "error" }
        })
      )
    })
  })

  describe("ErrorClass", () => {
    it("make with void input", () => {
      class E extends Schema.ErrorClass<E>("E")({}) {}
      deepStrictEqual(E.make(), new E())
      deepStrictEqual(E.makeOption(), Option.some(new E()))
      deepStrictEqual(Effect.runSync(E.makeEffect()), new E())
    })

    it("fields argument", async () => {
      class E extends Schema.ErrorClass<E>("E")({
        id: Schema.Number
      }) {}
      const asserts = new TestSchema.Asserts(E)

      const err = new E({ id: 1 })

      strictEqual(String(err), `E`)
      assertInclude(err.stack, "Schema.test.ts:")
      strictEqual(err.id, 1)

      const make = asserts.make()
      await make.succeed(new E({ id: 1 }))
      await make.succeed({ id: 1 }, new E({ id: 1 }))
    })

    it("constructor ignores excess properties by default", () => {
      class E extends Schema.ErrorClass<E>("E")({
        message: Schema.String,
        cause: Schema.optionalKey(Schema.Unknown),
        code: Schema.Number
      }) {}
      const cause = new Error("cause")

      const err = new E({ message: "boom", cause, code: 1, extra: "extra" } as any)

      strictEqual(err.message, "boom")
      strictEqual(err.cause, cause)
      strictEqual(err.code, 1)
      assertFalse("extra" in err)
    })

    it("constructor preserves excess properties when requested", () => {
      class E extends Schema.ErrorClass<E>("E")({
        message: Schema.String,
        code: Schema.Number
      }) {}

      const err = new E({ message: "boom", code: 1, extra: "extra" } as any, {
        parseOptions: { onExcessProperty: "preserve" }
      })

      strictEqual(err.message, "boom")
      strictEqual(err.code, 1)
      strictEqual((err as any).extra, "extra")
    })

    it("Struct argument", async () => {
      class E extends Schema.ErrorClass<E>("E")(Schema.Struct({
        id: Schema.Number
      })) {}
      const asserts = new TestSchema.Asserts(E)

      const err = new E({ id: 1 })

      strictEqual(String(err), `E`)
      assertInclude(err.stack, "Schema.test.ts:")
      strictEqual(err.id, 1)

      const make = asserts.make()
      await make.succeed(new E({ id: 1 }))
      await make.succeed({ id: 1 }, new E({ id: 1 }))

      if (verifyGeneration) {
        asserts.arbitrary().verifyGeneration()
      }
    })

    it("extend", async () => {
      class A extends Schema.ErrorClass<A>("A")({
        a: Schema.String
      }) {
        readonly _a = 1
      }
      class B extends A.extend<B>("B")({
        b: Schema.Number
      }) {
        readonly _b = 2
      }
      const asserts = new TestSchema.Asserts(B)

      const instance = new B({ a: "a", b: 2 })

      strictEqual(String(instance), `B`)
      assertInclude(instance.stack, "Schema.test.ts:")

      assertTrue(instance instanceof A)
      assertTrue(B.make({ a: "a", b: 2 }) instanceof A)
      assertTrue(instance instanceof B)
      assertTrue(B.make({ a: "a", b: 2 }) instanceof B)

      strictEqual(instance.a, "a")
      strictEqual(instance._a, 1)
      strictEqual(instance.b, 2)
      strictEqual(instance._b, 2)

      const make = asserts.make()
      await make.succeed(new B({ a: "a", b: 2 }))
      await make.succeed({ a: "a", b: 2 }, new B({ a: "a", b: 2 }))

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "a", b: 2 }, new B({ a: "a", b: 2 }))
    })

    it("extended constructor ignores excess properties by default", () => {
      class A extends Schema.ErrorClass<A>("A")({
        message: Schema.String
      }) {}
      class B extends A.extend<B>("B")({
        code: Schema.Number
      }) {}

      const err = new B({ message: "boom", code: 1, extra: "extra" } as any)

      strictEqual(err.message, "boom")
      strictEqual(err.code, 1)
      assertFalse("extra" in err)
    })

    it("extended constructor does not treat subclass fields as excess properties", () => {
      class A extends Schema.ErrorClass<A>("A")({
        message: Schema.String
      }) {}
      class B extends A.extend<B>("B")({
        code: Schema.Number
      }) {}

      const err = new B({ message: "boom", code: 1 }, {
        parseOptions: { onExcessProperty: "error" }
      })

      strictEqual(err.message, "boom")
      strictEqual(err.code, 1)
    })

    it("`toString` to match native `Error` output format", async () => {
      class E extends Schema.ErrorClass<E>("E")({
        message: Schema.String
      }) {}
      const err = new E({ message: "my message" })
      strictEqual(String(err), `E: my message`)
    })
  })

  describe("TaggedErrorClass", () => {
    it("make with void input", () => {
      class E extends Schema.TaggedErrorClass<E>()("E", {}) {}
      deepStrictEqual(E.make(), new E())
      deepStrictEqual(E.makeOption(), Option.some(new E()))
      deepStrictEqual(Effect.runSync(E.makeEffect()), new E())
    })

    it("fields argument", async () => {
      class E extends Schema.TaggedErrorClass<E>()("E", {
        id: Schema.Number
      }) {}
      const asserts = new TestSchema.Asserts(E)

      const err = new E({ id: 1 })
      strictEqual(err._tag, "E")
      strictEqual(err.id, 1)
      assertInclude(err.stack, "Schema.test.ts:")

      const make = asserts.make()
      await make.succeed(new E({ id: 1 }))
      await make.succeed({ id: 1 }, new E({ id: 1 }))

      const decoding = asserts.decoding()
      await decoding.succeed({ _tag: "E", id: 1 }, new E({ id: 1 }))

      const encoding = asserts.encoding()
      await encoding.succeed(new E({ id: 1 }), { _tag: "E", id: 1 })
    })

    it("constructor ignores excess properties by default", () => {
      class E extends Schema.TaggedErrorClass<E>()("E", {
        id: Schema.Number
      }) {}

      const err = new E({ id: 1, extra: "extra" } as any)

      strictEqual(err._tag, "E")
      strictEqual(err.id, 1)
      assertFalse("extra" in err)
    })

    it("Struct argument", async () => {
      class E extends Schema.TaggedErrorClass<E>()(
        "E",
        Schema.Struct({
          id: Schema.Number
        })
      ) {}

      const err = new E({ id: 1 })
      strictEqual(err._tag, "E")
      strictEqual(err.id, 1)
      deepStrictEqual(Object.keys(E.fields).sort(), ["_tag", "id"])
    })

    it("name matches tag", () => {
      class E extends Schema.TaggedErrorClass<E>()("TaggedErrorName", {
        id: Schema.Number
      }) {}

      const err = new E({ id: 1 })
      strictEqual(err.name, "TaggedErrorName")
    })

    it("name matches identifier", () => {
      class E extends Schema.TaggedErrorClass<E>("A")("B", {
        a: Schema.Number
      }) {}

      const err = new E({ a: 1 })
      strictEqual(err.name, "A")
    })

    it("name matches identifier after extend", () => {
      class E extends Schema.TaggedErrorClass<E>("A")("B", {
        a: Schema.Number
      }) {}
      class E2 extends E.extend<E2>("C")({
        b: Schema.String
      }) {}

      const err = new E2({ a: 1, b: "b" })
      strictEqual(err.name, "C")
    })

    it("zero-field TaggedErrorClass allows omitting props argument", () => {
      class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("NotFoundError", {}) {}

      // new NotFoundError() should work without passing {}
      const a = new NotFoundError()
      strictEqual(a._tag, "NotFoundError")
      assertTrue(a instanceof NotFoundError)

      // new NotFoundError({}) should also still work
      const b = new NotFoundError({})
      strictEqual(b._tag, "NotFoundError")
      assertTrue(b instanceof NotFoundError)
    })

    it("extend", async () => {
      class A extends Schema.TaggedErrorClass<A>()("A", {
        a: Schema.String
      }) {}
      class B extends A.extend<B>("B")({
        b: Schema.Number
      }) {}
      const instance = new B({ a: "a", b: 2 })
      strictEqual(instance._tag, "A")
      assertTrue(instance instanceof A)
      assertTrue(instance instanceof B)
    })

    it("extended constructor ignores excess properties by default", () => {
      class A extends Schema.TaggedErrorClass<A>()("A", {
        a: Schema.String
      }) {}
      class B extends A.extend<B>("B")({
        b: Schema.Number
      }) {}

      const instance = new B({ a: "a", b: 2, extra: "extra" } as any)

      strictEqual(instance._tag, "A")
      strictEqual(instance.a, "a")
      strictEqual(instance.b, 2)
      assertFalse("extra" in instance)
    })

    it("extended constructor does not treat subclass fields as excess properties", () => {
      class A extends Schema.TaggedErrorClass<A>()("A", {
        a: Schema.String
      }) {}
      class B extends A.extend<B>("B")({
        b: Schema.Number
      }) {}

      const instance = new B({ a: "a", b: 2 }, {
        parseOptions: { onExcessProperty: "error" }
      })

      strictEqual(instance._tag, "A")
      strictEqual(instance.a, "a")
      strictEqual(instance.b, 2)
    })
  })

  describe("Enum", () => {
    it("enums should be exposed", () => {
      enum Fruits {
        Apple,
        Banana
      }
      const schema = Schema.Enum(Fruits)
      strictEqual(schema.enums.Apple, 0)
      strictEqual(schema.enums.Banana, 1)
    })

    it("Numeric enum", async () => {
      enum Fruits {
        Apple,
        Banana
      }
      const schema = Schema.Enum(Fruits)
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(Fruits.Apple)
      await decoding.succeed(Fruits.Banana)
      await decoding.succeed(0)
      await decoding.succeed(1)

      await decoding.fail(
        3,
        `Expected 0 | 1, got 3`
      )

      const encoding = asserts.encoding()
      await encoding.succeed(Fruits.Apple, 0)
      await encoding.succeed(Fruits.Banana, 1)
    })

    it("String enum", async () => {
      enum Fruits {
        Apple = "apple",
        Banana = "banana",
        Cantaloupe = 0
      }
      const schema = Schema.Enum(Fruits)
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(Fruits.Apple)
      await decoding.succeed(Fruits.Cantaloupe)
      await decoding.succeed("apple")
      await decoding.succeed("banana")
      await decoding.succeed(0)
      await decoding.succeed(0)

      await decoding.fail(
        "Cantaloupe",
        `Expected "apple" | "banana" | 0, got "Cantaloupe"`
      )

      const encoding = asserts.encoding()
      await encoding.succeed(Fruits.Apple)
      await encoding.succeed(Fruits.Banana)
      await encoding.succeed(Fruits.Cantaloupe)
    })

    it("Const enum", async () => {
      const Fruits = {
        Apple: "apple",
        Banana: "banana",
        Cantaloupe: 3
      } as const
      const schema = Schema.Enum(Fruits)
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("apple")
      await decoding.succeed("banana")
      await decoding.succeed(3)

      await decoding.fail(
        "Cantaloupe",
        `Expected "apple" | "banana" | 3, got "Cantaloupe"`
      )

      const encoding = asserts.encoding()
      await encoding.succeed(Fruits.Apple, "apple")
      await encoding.succeed(Fruits.Banana, "banana")
      await encoding.succeed(Fruits.Cantaloupe, 3)
    })
  })

  describe("catchDecoding", () => {
    it("sync fallback", async () => {
      const fallback = Effect.succeed(Option.some("b"))
      const schema = Schema.String.pipe(Schema.catchDecoding(() => fallback)).check(Schema.isNonEmpty())
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a")
      await decoding.succeed(null, "b")
      await decoding.fail(
        "",
        `Expected a value with a length of at least 1, got ""`
      )

      const encoding = asserts.encoding()
      await encoding.succeed("a")
      await encoding.fail(
        null,
        "Expected string, got null"
      )
    })

    it("async fallback", async () => {
      const fallback = Effect.succeed(Option.some("b")).pipe(Effect.delay(100))
      const schema = Schema.String.pipe(Schema.catchDecoding(() => fallback))
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a")
      await decoding.succeed(null, "b")
    })
  })

  it("catchDecodingWithContext", async () => {
    class Service extends Context.Service<Service, { fallback: Effect.Effect<string> }>()("Service") {}

    const schema = Schema.String.pipe(Schema.catchDecodingWithContext(() =>
      Effect.gen(function*() {
        const service = yield* Service
        return Option.some(yield* service.fallback)
      })
    ))
    const asserts = new TestSchema.Asserts(schema)

    const decoding = asserts.decoding().provide(
      Service,
      { fallback: Effect.succeed("b") }
    )
    await decoding.succeed("a")
    await decoding.succeed(null, "b")
  })

  describe("middlewareDecoding", () => {
    it("providing a service", async () => {
      class Service extends Context.Service<Service, { fallback: Effect.Effect<number> }>()("Service") {}

      const schema = Schema.FiniteFromString.pipe(
        Schema.catchDecodingWithContext((issue) =>
          Effect.gen(function*() {
            if (issue._tag === "Encoding" && issue.issue._tag === "InvalidType") {
              return yield* Effect.fail(issue)
            }
            const service = yield* Service
            return Option.some(yield* service.fallback)
          })
        ),
        Schema.middlewareDecoding(Effect.provideService(Service, { fallback: Effect.succeed(0) }))
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("1", 1)
      await decoding.succeed("a", 0)
      await decoding.fail(null, "Expected string, got null")
    })

    it("forced failure", async () => {
      const schema = Schema.String.pipe(
        Schema.middlewareDecoding(() =>
          Effect.fail(new SchemaIssue.Forbidden(Option.none(), { message: "my message" }))
        )
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.fail(
        "1",
        "my message"
      )
    })
  })

  describe("catchEncoding", () => {
    it("sync fallback", async () => {
      const fallback = Effect.succeed(Option.some(0))
      const schema = Schema.Number.pipe(Schema.catchEncoding(() => fallback), Schema.encodeTo(Schema.Int))
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(1)
      await decoding.fail(
        1.2,
        `Expected an integer, got 1.2`
      )

      const encoding = asserts.encoding()
      await encoding.succeed(1)
      await encoding.succeed(null, 0)
      await encoding.fail(
        1.2,
        `Expected an integer, got 1.2`
      )
    })

    it("async fallback", async () => {
      const fallback = Effect.succeed(Option.some(0)).pipe(Effect.delay(100))
      const schema = Schema.Number.pipe(Schema.catchEncoding(() => fallback), Schema.encodeTo(Schema.Int))
      const asserts = new TestSchema.Asserts(schema)

      const encoding = asserts.encoding()
      await encoding.succeed(1)
      await encoding.succeed(null, 0)
      await encoding.fail(
        1.2,
        `Expected an integer, got 1.2`
      )
    })
  })

  it("catchEncodingWithContext", async () => {
    class Service extends Context.Service<Service, { fallback: Effect.Effect<number> }>()("Service") {}

    const schema = Schema.Number.pipe(
      Schema.catchEncodingWithContext(() =>
        Effect.gen(function*() {
          const service = yield* Service
          return Option.some(yield* service.fallback)
        })
      ),
      Schema.encodeTo(Schema.Int)
    )
    const asserts = new TestSchema.Asserts(schema)

    const encoding = asserts.encoding().provide(
      Service,
      { fallback: Effect.succeed(0) }
    )
    await encoding.succeed(1)
    await encoding.succeed(null, 0)
    await encoding.fail(
      1.2,
      `Expected an integer, got 1.2`
    )
  })

  describe("middlewareEncoding", () => {
    it("providing a service", async () => {
      class Service extends Context.Service<Service, { fallback: Effect.Effect<string> }>()("Service") {}

      const schema = Schema.FiniteFromString.pipe(
        Schema.catchEncodingWithContext((issue) =>
          Effect.gen(function*() {
            if (issue._tag === "InvalidType") {
              return yield* Effect.fail(issue)
            }
            const service = yield* Service
            return Option.some(yield* service.fallback)
          })
        ),
        Schema.middlewareEncoding(Effect.provideService(Service, { fallback: Effect.succeed("b") }))
      )
      const asserts = new TestSchema.Asserts(schema)

      const encoding = asserts.encoding()
      await encoding.succeed(1, "1")
      await encoding.succeed(NaN, "b")
      await encoding.fail(null, "Expected number, got null")
    })

    it("forced failure", async () => {
      const schema = Schema.String.pipe(
        Schema.middlewareEncoding(() =>
          Effect.fail(new SchemaIssue.Forbidden(Option.none(), { message: "my message" }))
        )
      )
      const asserts = new TestSchema.Asserts(schema)

      const encoding = asserts.encoding()
      await encoding.fail(1, "my message")
    })
  })

  describe("Optional Fields", () => {
    it("Exact Optional Property", async () => {
      const schema = Schema.Struct({
        a: Schema.optionalKey(Schema.FiniteFromString)
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "1" }, { a: 1 })
      await decoding.succeed({})

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1 }, { a: "1" })
      await encoding.succeed({})
    })

    it("Optional Property", async () => {
      const schema = Schema.Struct({
        a: Schema.optional(Schema.FiniteFromString)
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "1" }, { a: 1 })
      await decoding.succeed({})
      await decoding.succeed({ a: undefined })

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1 }, { a: "1" })
      await encoding.succeed({})
      await encoding.succeed({ a: undefined })
    })

    it("Exact Optional Property with Nullability", async () => {
      const schema = Schema.Struct({
        a: Schema.optionalKey(Schema.NullOr(Schema.FiniteFromString))
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "1" }, { a: 1 })
      await decoding.succeed({})
      await decoding.succeed({ a: null })

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1 }, { a: "1" })
      await encoding.succeed({})
      await encoding.succeed({ a: null })
    })

    it("Optional Property with Nullability", async () => {
      const schema = Schema.Struct({
        a: Schema.optional(Schema.NullOr(Schema.FiniteFromString))
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "1" }, { a: 1 })
      await decoding.succeed({})
      await decoding.succeed({ a: undefined })
      await decoding.succeed({ a: null })

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1 }, { a: "1" })
      await encoding.succeed({})
      await encoding.succeed({ a: null })
      await encoding.succeed({ a: undefined })
    })

    it("Optional Property to Exact Optional Property", async () => {
      const schema = Schema.Struct({
        a: Schema.optional(Schema.FiniteFromString).pipe(Schema.decodeTo(Schema.optionalKey(Schema.Number), {
          decode: SchemaGetter.transformOptional(Option.filter(Predicate.isNotUndefined)),
          encode: SchemaGetter.passthrough()
        }))
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "1" }, { a: 1 })
      await decoding.succeed({})
      await decoding.succeed({ a: undefined }, {})

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1 }, { a: "1" })
      await encoding.succeed({})
    })

    it("Optional Property with Nullability to Optional Property", async () => {
      const schema = Schema.Struct({
        a: Schema.optional(Schema.NullOr(Schema.FiniteFromString)).pipe(
          Schema.decodeTo(Schema.optional(Schema.Number), {
            decode: SchemaGetter.transformOptional(Option.filter(Predicate.isNotNull)),
            encode: SchemaGetter.passthrough()
          })
        )
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "1" }, { a: 1 })
      await decoding.succeed({})
      await decoding.succeed({ a: undefined })
      await decoding.succeed({ a: null }, {})

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1 }, { a: "1" })
      await encoding.succeed({})
    })
  })

  describe("asOption", () => {
    it("optionalKey -> Option", async () => {
      const schema = Schema.Struct({
        a: Schema.optionalKey(Schema.FiniteFromString).pipe(
          Schema.decodeTo(
            Schema.Option(Schema.Number),
            SchemaTransformation.transformOptional({
              decode: Option.some,
              encode: Option.flatten
            })
          )
        )
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "1" }, { a: Option.some(1) })
      await decoding.succeed({}, { a: Option.none() })

      const encoding = asserts.encoding()
      await encoding.succeed({ a: Option.some(1) }, { a: "1" })
      await encoding.succeed({ a: Option.none() }, {})
    })

    it("optional -> Option", async () => {
      const schema = Schema.Struct({
        a: Schema.optional(Schema.FiniteFromString).pipe(
          Schema.decodeTo(
            Schema.Option(Schema.Number),
            SchemaTransformation.transformOptional({
              decode: (on) => on.pipe(Option.filter((nu) => nu !== undefined), Option.some),
              encode: Option.flatten
            })
          )
        )
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "1" }, { a: Option.some(1) })
      await decoding.succeed({}, { a: Option.none() })
      await decoding.succeed({ a: undefined }, { a: Option.none() })

      const encoding = asserts.encoding()
      await encoding.succeed({ a: Option.some(1) }, { a: "1" })
      await encoding.succeed({ a: Option.none() }, {})
    })
  })

  it("decodeTo as composition", async () => {
    const From = Schema.Struct({
      a: Schema.String,
      b: Schema.FiniteFromString
    })

    const To = Schema.Struct({
      a: Schema.FiniteFromString,
      b: Schema.UndefinedOr(Schema.Number)
    })

    const schema = From.pipe(Schema.decodeTo(To))
    const asserts = new TestSchema.Asserts(schema)

    const decoding = asserts.decoding()
    await decoding.succeed({ a: "1", b: "2" }, { a: 1, b: 2 })

    const encoding = asserts.encoding()
    await encoding.succeed({ a: 1, b: 2 }, { a: "1", b: "2" })
    await encoding.fail(
      { a: 1, b: NaN },
      `Expected a finite number, got NaN
  at ["b"]`
    )
    await encoding.fail(
      { a: 1, b: undefined },
      `Expected number, got undefined
  at ["b"]`
    )
  })

  it("encodeTo as composition", async () => {
    const From = Schema.Struct({
      a: Schema.String,
      b: Schema.FiniteFromString
    })

    const To = Schema.Struct({
      a: Schema.FiniteFromString,
      b: Schema.UndefinedOr(Schema.Number)
    })

    const schema = To.pipe(Schema.encodeTo(From))
    const asserts = new TestSchema.Asserts(schema)

    const decoding = asserts.decoding()
    await decoding.succeed({ a: "1", b: "2" }, { a: 1, b: 2 })
    await decoding.fail(
      { a: "1", b: null },
      `Expected string, got null
  at ["b"]`
    )

    const encoding = asserts.encoding()
    await encoding.succeed({ a: 1, b: 2 }, { a: "1", b: "2" })
    await encoding.fail(
      { a: 1, b: NaN },
      `Expected a finite number, got NaN
  at ["b"]`
    )
    await encoding.fail(
      { a: 1, b: undefined },
      `Expected number, got undefined
  at ["b"]`
    )
  })

  describe("checkEffect", () => {
    it("no context", async () => {
      const schema = Schema.String.pipe(
        Schema.decode({
          decode: SchemaGetter.checkEffect((s) =>
            Effect.gen(function*() {
              if (s.length === 0) {
                return new SchemaIssue.InvalidValue(Option.some(s), { message: "input should not be empty string" })
              }
            }).pipe(Effect.delay(100))
          ),
          encode: SchemaGetter.passthrough()
        })
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a")
      await decoding.fail(
        "",
        "input should not be empty string"
      )
    })

    it("with context", async () => {
      class Service extends Context.Service<Service, { fallback: Effect.Effect<string> }>()("Service") {}

      const schema = Schema.String.pipe(
        Schema.decode({
          decode: SchemaGetter.checkEffect((s) =>
            Effect.gen(function*() {
              yield* Service
              if (s.length === 0) {
                return new SchemaIssue.InvalidValue(Option.some(s), { message: "input should not be empty string" })
              }
            })
          ),
          encode: SchemaGetter.passthrough()
        })
      )
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding().provide(
        Service,
        { fallback: Effect.succeed("b") }
      )
      await decoding.succeed("a")
      await decoding.fail(
        "",
        "input should not be empty string"
      )
    })
  })

  describe("is", () => {
    it("FiniteFromString", () => {
      const schema = Schema.FiniteFromString
      const is = Schema.is(schema)
      assertTrue(is(1))
      assertFalse(is("a"))
    })
  })

  describe("asserts", () => {
    it("FiniteFromString", () => {
      const schema = Schema.FiniteFromString
      try {
        Schema.asserts(schema, 1)
      } catch {
        fail("Expected asserts to not throw an error")
      }
      try {
        Schema.asserts(schema, "a")
        fail("Expected asserts to throw an error")
      } catch (e) {
        ok(e instanceof Error)
        strictEqual(e.message, `Expected number, got "a"`)
      }
    })
  })

  describe("decodeUnknownPromise / encodeUnknownPromise", () => {
    it("FiniteFromString", async () => {
      const schema = Schema.FiniteFromString
      const decodeUnknownPromise = Schema.decodeUnknownPromise(schema)
      const encodeUnknownPromise = Schema.encodeUnknownPromise(schema)
      const decodeUnknownPromiseIssue = SchemaParser.decodeUnknownPromise(schema)
      const encodeUnknownPromiseIssue = SchemaParser.encodeUnknownPromise(schema)

      const r1 = await decodeUnknownPromise("1").then(Result.succeed, Result.fail)
      deepStrictEqual(r1, Result.succeed(1))

      const r2 = await decodeUnknownPromise(null).then(Result.succeed, Result.fail)
      assertTrue(Result.isFailure(r2))
      assertTrue(Schema.isSchemaError(r2.failure))
      strictEqual(r2.failure.message, "Expected string, got null")

      const r3 = await encodeUnknownPromise(1).then(Result.succeed, Result.fail)
      deepStrictEqual(r3, Result.succeed("1"))

      const r4 = await encodeUnknownPromise(null).then(Result.succeed, Result.fail)
      assertTrue(Result.isFailure(r4))
      assertTrue(Schema.isSchemaError(r4.failure))
      strictEqual(r4.failure.message, "Expected number, got null")

      const r5 = await decodeUnknownPromiseIssue(null).then(Result.succeed, Result.fail)
      assertTrue(Result.isFailure(r5))
      assertTrue(r5.failure instanceof Error)
      strictEqual(r5.failure.message, "Expected string, got null")

      const r6 = await encodeUnknownPromiseIssue(null).then(Result.succeed, Result.fail)
      assertTrue(Result.isFailure(r6))
      assertTrue(r6.failure instanceof Error)
      strictEqual(r6.failure.message, "Expected number, got null")
    })

    it("should reject with an error when the cause contains both a schema issue and a defect", async () => {
      const cause = Cause.combine(
        Cause.fail(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" })),
        Cause.die(new Error("defect"))
      )
      const decodeSchema = Schema.String.pipe(Schema.decode({
        decode: new SchemaGetter.Getter(() => Effect.failCause(cause)),
        encode: SchemaGetter.passthrough()
      }))
      const encodeSchema = Schema.String.pipe(Schema.encode({
        decode: SchemaGetter.passthrough(),
        encode: new SchemaGetter.Getter(() => Effect.failCause(cause))
      }))

      const r1 = await Schema.decodeUnknownPromise(decodeSchema)("a").then(Result.succeed, Result.fail)
      assertTrue(Result.isFailure(r1))
      assertTrue(r1.failure instanceof Error)
      strictEqual(r1.failure.message, "Promise adapter can only reject schema errors")
      assertTrue(Cause.hasDies(r1.failure.cause as Cause.Cause<never>))

      const r2 = await Schema.encodeUnknownPromise(encodeSchema)("a").then(Result.succeed, Result.fail)
      assertTrue(Result.isFailure(r2))
      assertTrue(r2.failure instanceof Error)
      strictEqual(r2.failure.message, "Promise adapter can only reject schema errors")
      assertTrue(Cause.hasDies(r2.failure.cause as Cause.Cause<never>))
    })
  })

  describe("decodeUnknownOption / encodeUnknownOption", () => {
    it("FiniteFromString", () => {
      const schema = Schema.FiniteFromString
      const decodeUnknownOption = Schema.decodeUnknownOption(schema)
      const encodeUnknownOption = Schema.encodeUnknownOption(schema)

      const r1 = decodeUnknownOption("1")
      assertTrue(Option.isSome(r1))
      strictEqual(r1.value, 1)

      assertTrue(Option.isNone(decodeUnknownOption(null)))

      const r2 = encodeUnknownOption(1)
      assertTrue(Option.isSome(r2))
      strictEqual(r2.value, "1")

      assertTrue(Option.isNone(encodeUnknownOption(null)))
    })

    it("should throw an error when the cause is not a schema issue", () => {
      const decodeSchema = Schema.String.pipe(Schema.decode({
        decode: new SchemaGetter.Getter(() => Effect.die(new Error("decode defect"))),
        encode: SchemaGetter.passthrough()
      }))
      const encodeSchema = Schema.String.pipe(Schema.encode({
        decode: SchemaGetter.passthrough(),
        encode: new SchemaGetter.Getter(() => Effect.die(new Error("encode defect")))
      }))

      throws(() => Schema.decodeUnknownOption(decodeSchema)("a"), (e) => {
        assertTrue(e instanceof Error)
        strictEqual(e.message, "Option adapter can only return none for schema issues")
        assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
      })
      throws(() => Schema.encodeUnknownOption(encodeSchema)("a"), (e) => {
        assertTrue(e instanceof Error)
        strictEqual(e.message, "Option adapter can only return none for schema issues")
        assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
      })
    })

    it("should throw an error when the cause contains both a schema issue and a defect", () => {
      const cause = Cause.combine(
        Cause.fail(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" })),
        Cause.die(new Error("defect"))
      )
      const decodeSchema = Schema.String.pipe(Schema.decode({
        decode: new SchemaGetter.Getter(() => Effect.failCause(cause)),
        encode: SchemaGetter.passthrough()
      }))
      const encodeSchema = Schema.String.pipe(Schema.encode({
        decode: SchemaGetter.passthrough(),
        encode: new SchemaGetter.Getter(() => Effect.failCause(cause))
      }))

      throws(() => Schema.decodeUnknownOption(decodeSchema)("a"), (e) => {
        assertTrue(e instanceof Error)
        strictEqual(e.message, "Option adapter can only return none for schema issues")
        assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
      })
      throws(() => Schema.encodeUnknownOption(encodeSchema)("a"), (e) => {
        assertTrue(e instanceof Error)
        strictEqual(e.message, "Option adapter can only return none for schema issues")
        assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
      })
    })
  })

  describe("decodeUnknownResult / encodeUnknownResult", () => {
    it("FiniteFromString", () => {
      const schema = Schema.FiniteFromString
      const decodeUnknownResult = Schema.decodeUnknownResult(schema)
      const encodeUnknownResult = Schema.encodeUnknownResult(schema)

      const r1 = decodeUnknownResult("1")
      assertTrue(Result.isSuccess(r1))
      strictEqual(r1.success, 1)

      const r2 = decodeUnknownResult(null)
      assertTrue(Result.isFailure(r2))
      assertTrue(Schema.isSchemaError(r2.failure))
      strictEqual(r2.failure.message, "Expected string, got null")

      const r3 = encodeUnknownResult(1)
      assertTrue(Result.isSuccess(r3))
      strictEqual(r3.success, "1")

      const r4 = encodeUnknownResult(null)
      assertTrue(Result.isFailure(r4))
      assertTrue(Schema.isSchemaError(r4.failure))
      strictEqual(r4.failure.message, "Expected number, got null")

      const r5 = SchemaParser.decodeUnknownResult(schema)(null)
      assertTrue(Result.isFailure(r5))
      assertTrue(SchemaIssue.isIssue(r5.failure))
      strictEqual(r5.failure.toString(), "Expected string, got null")

      const r6 = SchemaParser.encodeUnknownResult(schema)(null)
      assertTrue(Result.isFailure(r6))
      assertTrue(SchemaIssue.isIssue(r6.failure))
      strictEqual(r6.failure.toString(), "Expected number, got null")
    })

    it("should throw an error when the cause contains both a schema issue and a defect", () => {
      const cause = Cause.combine(
        Cause.fail(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" })),
        Cause.die(new Error("defect"))
      )
      const decodeSchema = Schema.String.pipe(Schema.decode({
        decode: new SchemaGetter.Getter(() => Effect.failCause(cause)),
        encode: SchemaGetter.passthrough()
      }))
      const encodeSchema = Schema.String.pipe(Schema.encode({
        decode: SchemaGetter.passthrough(),
        encode: new SchemaGetter.Getter(() => Effect.failCause(cause))
      }))

      throws(() => Schema.decodeUnknownResult(decodeSchema)("a"), (e) => {
        assertTrue(e instanceof Error)
        strictEqual(e.message, "Result adapter can only return schema issues")
        assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
      })
      throws(() => Schema.encodeUnknownResult(encodeSchema)("a"), (e) => {
        assertTrue(e instanceof Error)
        strictEqual(e.message, "Result adapter can only return schema issues")
        assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
      })
    })
  })

  describe("decodeUnknownSync / encodeUnknownSync", () => {
    it("FiniteFromString", () => {
      const schema = Schema.FiniteFromString

      strictEqual(Schema.decodeUnknownSync(schema)("1"), 1)
      strictEqual(Schema.encodeUnknownSync(schema)(1), "1")

      throws(() => Schema.decodeUnknownSync(schema)(null), (e) => {
        assertTrue(Schema.isSchemaError(e))
        strictEqual(e.message, "Expected string, got null")
      })

      throws(() => Schema.encodeUnknownSync(schema)(null), (e) => {
        assertTrue(Schema.isSchemaError(e))
        strictEqual(e.message, "Expected number, got null")
      })

      throws(() => SchemaParser.decodeUnknownSync(schema)(null), (e) => {
        assertTrue(e instanceof Error)
        assertTrue(SchemaIssue.isIssue(e.cause))
        strictEqual(e.cause.toString(), "Expected string, got null")
      })

      throws(() => SchemaParser.encodeUnknownSync(schema)(null), (e) => {
        assertTrue(e instanceof Error)
        assertTrue(SchemaIssue.isIssue(e.cause))
        strictEqual(e.cause.toString(), "Expected number, got null")
      })
    })

    it("should throw an error when the cause contains both a schema issue and a defect", () => {
      const cause = Cause.combine(
        Cause.fail(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" })),
        Cause.die(new Error("defect"))
      )
      const decodeSchema = Schema.String.pipe(Schema.decode({
        decode: new SchemaGetter.Getter(() => Effect.failCause(cause)),
        encode: SchemaGetter.passthrough()
      }))
      const encodeSchema = Schema.String.pipe(Schema.encode({
        decode: SchemaGetter.passthrough(),
        encode: new SchemaGetter.Getter(() => Effect.failCause(cause))
      }))

      throws(() => Schema.decodeUnknownSync(decodeSchema)("a"), (e) => {
        assertTrue(e instanceof Error)
        strictEqual(e.message, "Sync adapter can only throw schema errors")
        assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
      })
      throws(() => Schema.encodeUnknownSync(encodeSchema)("a"), (e) => {
        assertTrue(e instanceof Error)
        strictEqual(e.message, "Sync adapter can only throw schema errors")
        assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
      })
    })
  })

  describe("decodeUnknownResult", () => {
    it("should throw on async decoding", () => {
      const AsyncString = Schema.String.pipe(Schema.decode({
        decode: new SchemaGetter.Getter((os: Option.Option<string>) =>
          Effect.gen(function*() {
            yield* Effect.sleep("10 millis")
            return os
          })
        ),
        encode: SchemaGetter.passthrough()
      }))
      const schema = AsyncString

      throws(() => SchemaParser.decodeUnknownResult(schema)("1"))
    })

    it("should throw on missing dependency", () => {
      class MagicNumber extends Context.Service<MagicNumber, number>()("MagicNumber") {}
      const DepString = Schema.Number.pipe(Schema.decode({
        decode: SchemaGetter.onSome((n) =>
          Effect.gen(function*() {
            const magicNumber = yield* MagicNumber
            return Option.some(n * magicNumber)
          })
        ),
        encode: SchemaGetter.passthrough()
      }))
      const schema = DepString

      throws(() => SchemaParser.decodeUnknownResult(schema as any)(1))
    })
  })

  describe("decodeUnknownExit", () => {
    it("should die on async decoding", () => {
      const AsyncString = Schema.String.pipe(Schema.decode({
        decode: new SchemaGetter.Getter((os: Option.Option<string>) =>
          Effect.gen(function*() {
            yield* Effect.sleep("10 millis")
            return os
          })
        ),
        encode: SchemaGetter.passthrough()
      }))
      const schema = AsyncString

      const exit = SchemaParser.decodeUnknownExit(schema)("1")
      assertTrue(Exit.hasDies(exit))
    })

    it("should die on missing dependency", () => {
      class MagicNumber extends Context.Service<MagicNumber, number>()("MagicNumber") {}
      const DepString = Schema.Number.pipe(Schema.decode({
        decode: SchemaGetter.onSome((n) =>
          Effect.gen(function*() {
            const magicNumber = yield* MagicNumber
            return Option.some(n * magicNumber)
          })
        ),
        encode: SchemaGetter.passthrough()
      }))
      const schema = DepString
      const exit = SchemaParser.decodeUnknownExit(schema as any)(1)
      assertTrue(Exit.hasDies(exit))
    })
  })

  describe("decodeUnknownExit / encodeUnknownExit", () => {
    it("should preserve mixed schema issue and defect causes", () => {
      const cause = Cause.combine(
        Cause.fail(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" })),
        Cause.die(new Error("defect"))
      )
      const decodeSchema = Schema.String.pipe(Schema.decode({
        decode: new SchemaGetter.Getter(() => Effect.failCause(cause)),
        encode: SchemaGetter.passthrough()
      }))
      const encodeSchema = Schema.String.pipe(Schema.encode({
        decode: SchemaGetter.passthrough(),
        encode: new SchemaGetter.Getter(() => Effect.failCause(cause))
      }))

      const decodeExit = Schema.decodeUnknownExit(decodeSchema)("a")
      assertTrue(Exit.isFailure(decodeExit))
      assertTrue(Exit.hasDies(decodeExit))
      const decodeError = Cause.findError(decodeExit.cause)
      assertTrue(Result.isSuccess(decodeError))
      assertTrue(Schema.isSchemaError(decodeError.success))

      const encodeExit = Schema.encodeUnknownExit(encodeSchema)("a")
      assertTrue(Exit.isFailure(encodeExit))
      assertTrue(Exit.hasDies(encodeExit))
      const encodeError = Cause.findError(encodeExit.cause)
      assertTrue(Result.isSuccess(encodeError))
      assertTrue(Schema.isSchemaError(encodeError.success))
    })
  })

  describe("annotateKey", () => {
    describe("the messageMissingKey annotation should be used as a error message", () => {
      it("Struct", async () => {
        const schema = Schema.Struct({
          a: Schema.String.pipe(Schema.annotateKey({ messageMissingKey: "this field is required" }))
        })
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.fail(
          {},
          `this field is required
  at ["a"]`
        )
      })

      it("Tuple", async () => {
        const schema = Schema.Tuple([
          Schema.String.pipe(Schema.annotateKey({ messageMissingKey: "this element is required" }))
        ])
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.fail(
          [],
          `this element is required
  at [0]`
        )
      })
    })
  })

  describe("annotateEncoded", () => {
    it("non-transforming schema", () => {
      const schema = Schema.String.pipe(
        Schema.annotateEncoded({ title: "encoded title" })
      )
      strictEqual(SchemaAST.toEncoded(schema.ast).annotations?.title, "encoded title")
    })

    it("transforming schema", () => {
      const schema = Schema.NumberFromString.pipe(
        Schema.annotateEncoded({ title: "encoded title" })
      )
      strictEqual(SchemaAST.toEncoded(schema.ast).annotations?.title, "encoded title")
    })
  })

  describe("Struct.mapFields", () => {
    it("evolve", () => {
      const schema = Schema.Struct({
        a: Schema.String,
        b: Schema.Number
      }).mapFields(Struct.evolve({ a: (v) => Schema.optionalKey(v) }))

      equals(schema.fields, {
        a: Schema.optionalKey(Schema.String),
        b: Schema.Number
      })
    })

    it("evolveKeys", () => {
      const schema = Schema.Struct({
        a: Schema.String,
        b: Schema.Number
      }).mapFields(Struct.evolveKeys({ a: (k) => Str.toUpperCase(k) }))

      equals(schema.fields, {
        A: Schema.String,
        b: Schema.Number
      })
    })

    it("renameKeys", () => {
      const schema = Schema.Struct({
        a: Schema.String,
        b: Schema.Number,
        c: Schema.Boolean
      }).mapFields(Struct.renameKeys({ a: "A", b: "B" }))

      equals(schema.fields, {
        A: Schema.String,
        B: Schema.Number,
        c: Schema.Boolean
      })
    })

    it("evolveEntries", () => {
      const schema = Schema.Struct({
        a: Schema.String,
        b: Schema.Number
      }).mapFields(Struct.evolveEntries({ a: (k, v) => [Str.toUpperCase(k), Schema.optionalKey(v)] }))

      equals(schema.fields, {
        A: Schema.optionalKey(Schema.String),
        b: Schema.Number
      })
    })

    it("optionalKey", () => {
      const schema = Schema.Struct({
        a: Schema.String,
        b: Schema.Number
      }).mapFields(Struct.map(Schema.optionalKey))

      equals(schema.fields, {
        a: Schema.optionalKey(Schema.String),
        b: Schema.optionalKey(Schema.Number)
      })
    })

    it("mapPick", () => {
      const schema = Schema.Struct({
        a: Schema.String,
        b: Schema.Number
      }).mapFields(Struct.mapPick(["a"], Schema.optionalKey))

      equals(schema.fields, {
        a: Schema.optionalKey(Schema.String),
        b: Schema.Number
      })
    })

    it("mapOmit", () => {
      const schema = Schema.Struct({
        a: Schema.String,
        b: Schema.Number
      }).mapFields(Struct.mapOmit(["b"], Schema.optionalKey))

      equals(schema.fields, {
        a: Schema.optionalKey(Schema.String),
        b: Schema.Number
      })
    })

    it("optional", () => {
      const schema = Schema.Struct({
        a: Schema.String,
        b: Schema.Number
      }).mapFields(Struct.map(Schema.optional))

      equals(schema.fields, {
        a: Schema.optional(Schema.String),
        b: Schema.optional(Schema.Number)
      })
    })

    it("mutableKey", () => {
      const schema = Schema.Struct({
        a: Schema.String,
        b: Schema.Number
      }).mapFields(Struct.map(Schema.mutableKey))

      equals(schema.fields, {
        a: Schema.mutableKey(Schema.String),
        b: Schema.mutableKey(Schema.Number)
      })
    })

    it("readonlyKey", () => {
      const schema = Schema.Struct({
        a: Schema.mutableKey(Schema.String),
        b: Schema.mutableKey(Schema.Number)
      }).mapFields(Struct.map(Schema.readonlyKey))

      equals(schema.fields, {
        a: Schema.String,
        b: Schema.Number
      })
    })

    it("mutable", () => {
      const schema = Schema.Struct({
        a: Schema.Array(Schema.String),
        b: Schema.Tuple([Schema.Number])
      }).mapFields(Struct.map(Schema.mutable))

      equals(schema.fields, {
        a: Schema.mutable(Schema.Array(Schema.String)),
        b: Schema.mutable(Schema.Tuple([Schema.Number]))
      })
    })

    it("NullOr", () => {
      const schema = Schema.Struct({
        a: Schema.String,
        b: Schema.Number
      }).mapFields(Struct.map(Schema.NullOr))

      equals(schema.fields, {
        a: Schema.NullOr(Schema.String),
        b: Schema.NullOr(Schema.Number)
      })
    })

    it("UndefinedOr", () => {
      const schema = Schema.Struct({
        a: Schema.String,
        b: Schema.Number
      }).mapFields(Struct.map(Schema.UndefinedOr))

      equals(schema.fields, {
        a: Schema.UndefinedOr(Schema.String),
        b: Schema.UndefinedOr(Schema.Number)
      })
    })

    it("NullishOr", () => {
      const schema = Schema.Struct({
        a: Schema.String,
        b: Schema.Number
      }).mapFields(Struct.map(Schema.NullishOr))

      equals(schema.fields, {
        a: Schema.NullishOr(Schema.String),
        b: Schema.NullishOr(Schema.Number)
      })
    })

    it("should work with flow", () => {
      const schema = Schema.Struct({
        a: Schema.String,
        b: Schema.FiniteFromString,
        c: Schema.Boolean
      }).mapFields(flow(
        Struct.map(Schema.NullOr),
        Struct.mapPick(["a", "c"], Schema.mutableKey)
      ))

      equals(schema.fields, {
        a: Schema.mutableKey(Schema.NullOr(Schema.String)),
        b: Schema.NullOr(Schema.FiniteFromString),
        c: Schema.mutableKey(Schema.NullOr(Schema.Boolean))
      })
    })
  })

  describe("Tuple.mapElements", () => {
    it("appendElement", () => {
      const schema = Schema.Tuple([Schema.String]).mapElements(Tuple.appendElement(Schema.Number))

      TestSchema.Asserts.ast.elements.equals(schema.elements, [Schema.String, Schema.Number])
    })

    it("appendElements", () => {
      const schema = Schema.Tuple([Schema.String]).mapElements(Tuple.appendElements([Schema.Number, Schema.Boolean]))

      TestSchema.Asserts.ast.elements.equals(schema.elements, [Schema.String, Schema.Number, Schema.Boolean])
    })

    it("pick", () => {
      const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements(Tuple.pick([0, 2]))

      TestSchema.Asserts.ast.elements.equals(schema.elements, [Schema.String, Schema.Boolean])
    })

    it("omit", () => {
      const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements(Tuple.omit([1]))

      TestSchema.Asserts.ast.elements.equals(schema.elements, [Schema.String, Schema.Boolean])
    })

    describe("evolve", () => {
      it("readonly [string] -> readonly [string?]", () => {
        const schema = Schema.Tuple([Schema.String]).mapElements(Tuple.evolve([(v) => Schema.optionalKey(v)]))

        TestSchema.Asserts.ast.elements.equals(schema.elements, [Schema.optionalKey(Schema.String)])
      })

      it("readonly [string, number] -> readonly [string, number?]", () => {
        const schema = Schema.Tuple([Schema.String, Schema.Number]).mapElements(
          Tuple.evolve([undefined, (v) => Schema.optionalKey(v)])
        )

        TestSchema.Asserts.ast.elements.equals(schema.elements, [Schema.String, Schema.optionalKey(Schema.Number)])
      })
    })

    describe("renameIndices", () => {
      it("partial index mapping", () => {
        const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements(
          Tuple.renameIndices(["1", "0"])
        )
        TestSchema.Asserts.ast.elements.equals(schema.elements, [Schema.Number, Schema.String, Schema.Boolean])
      })

      it("full index mapping", () => {
        const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements(
          Tuple.renameIndices(["2", "1", "0"])
        )
        TestSchema.Asserts.ast.elements.equals(schema.elements, [Schema.Boolean, Schema.Number, Schema.String])
      })
    })

    it("NullOr", () => {
      const schema = Schema.Tuple([Schema.String, Schema.Number]).mapElements(Tuple.map(Schema.NullOr))

      TestSchema.Asserts.ast.elements.equals(schema.elements, [
        Schema.NullOr(Schema.String),
        Schema.NullOr(Schema.Number)
      ])
    })
  })

  describe("Union.mapMembers", () => {
    it("appendElement", () => {
      const schema = Schema.Union([Schema.String, Schema.Number]).mapMembers(Tuple.appendElement(Schema.Boolean))

      TestSchema.Asserts.ast.elements.equals(schema.members, [Schema.String, Schema.Number, Schema.Boolean])
    })

    it("evolve", () => {
      const schema = Schema.Union([Schema.String, Schema.Number, Schema.Boolean]).mapMembers(
        Tuple.evolve([
          (v) => Schema.Array(v),
          undefined,
          (v) => Schema.Array(v)
        ])
      )

      TestSchema.Asserts.ast.elements.equals(schema.members, [
        Schema.Array(Schema.String),
        Schema.Number,
        Schema.Array(Schema.Boolean)
      ])
    })

    it("Array", () => {
      const schema = Schema.Union([Schema.String, Schema.Number]).mapMembers(Tuple.map(Schema.Array))

      TestSchema.Asserts.ast.elements.equals(schema.members, [
        Schema.Array(Schema.String),
        Schema.Array(Schema.Number)
      ])
    })
  })

  describe("Literals.mapMembers", () => {
    it("evolve", () => {
      const schema = Schema.Literals(["a", "b", "c"]).mapMembers(Tuple.evolve([
        (a) => Schema.Struct({ _tag: a, a: Schema.String }),
        (b) => Schema.Struct({ _tag: b, b: Schema.Number }),
        (c) => Schema.Struct({ _tag: c, c: Schema.Boolean })
      ]))

      TestSchema.Asserts.ast.elements.equals(schema.members, [
        Schema.Struct({ _tag: Schema.Literal("a"), a: Schema.String }),
        Schema.Struct({ _tag: Schema.Literal("b"), b: Schema.Number }),
        Schema.Struct({ _tag: Schema.Literal("c"), c: Schema.Boolean })
      ])
    })
  })

  describe("encodeKeys", () => {
    it("Struct", async () => {
      const schema = Schema.Struct({
        a: Schema.FiniteFromString,
        b: Schema.FiniteFromString
      }).pipe(Schema.encodeKeys({ a: "c" }))
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ c: "1", b: "2" }, { a: 1, b: 2 })

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1, b: 2 }, { c: "1", b: "2" })
    })

    it("Class", async () => {
      class A extends Schema.Class<A>("A")({
        a: Schema.FiniteFromString,
        b: Schema.String
      }) {}
      const schema = A.pipe(Schema.encodeKeys({ a: "c" }))
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ c: "1", b: "b" }, new A({ a: 1, b: "b" }))

      const encoding = asserts.encoding()
      await encoding.succeed(new A({ a: 1, b: "b" }), { c: "1", b: "b" })
    })

    it("supports symbol source keys", () => {
      const field = Symbol("field")
      const schema = Schema.Struct({
        [field]: Schema.String
      }).pipe(Schema.encodeKeys({ [field]: "field" }))

      deepStrictEqual(Schema.decodeUnknownSync(schema)({ field: "a" }), { [field]: "a" })
      deepStrictEqual(Schema.encodeSync(schema)({ [field]: "a" }), { field: "a" })
    })

    it("supports symbol destination keys", () => {
      const field = Symbol("field")
      const schema = Schema.Struct({
        field: Schema.String
      }).pipe(Schema.encodeKeys({ field }))

      deepStrictEqual(Schema.decodeUnknownSync(schema)({ [field]: "a" }), { field: "a" })
      deepStrictEqual(Schema.encodeSync(schema)({ field: "a" }), { [field]: "a" })
    })

    it("rejects duplicate destination keys", () => {
      throws(
        () =>
          Schema.Struct({
            a: Schema.String,
            b: Schema.String
          }).pipe(Schema.encodeKeys({ a: "c", b: "c" })),
        (e) => {
          assertInclude(String(e), "Duplicate encoded keys")
        }
      )
    })

    it("rejects destination keys that collide with unmapped fields", () => {
      throws(
        () =>
          Schema.Struct({
            a: Schema.String,
            b: Schema.String
          }).pipe(Schema.encodeKeys({ a: "b" })),
        (e) => {
          assertInclude(String(e), "Duplicate encoded keys")
        }
      )
    })

    it("rejects canonical number and string destination key collisions", () => {
      throws(
        () =>
          Schema.Struct({
            a: Schema.String,
            b: Schema.String
          }).pipe(Schema.encodeKeys({ a: 1, b: "1" })),
        (e) => {
          assertInclude(String(e), "Duplicate encoded keys")
        }
      )
    })
  })

  describe("Schema.makeFilter", () => {
    it("returns undefined", async () => {
      const schema = Schema.String.check(Schema.makeFilter(() => undefined))
      const asserts = new TestSchema.Asserts(schema)
      const decoding = asserts.decoding()
      await decoding.succeed("a")
    })

    it("returns true", async () => {
      const schema = Schema.String.check(Schema.makeFilter(() => true))
      const asserts = new TestSchema.Asserts(schema)
      const decoding = asserts.decoding()
      await decoding.succeed("a")
    })

    it("returns false", async () => {
      const schema = Schema.String.check(Schema.makeFilter(() => false))
      const asserts = new TestSchema.Asserts(schema)
      const decoding = asserts.decoding()
      await decoding.fail(
        "a",
        `Expected <filter>, got "a"`
      )
    })

    it("returns string", async () => {
      const schema = Schema.String.check(Schema.makeFilter(() => "error message"))
      const asserts = new TestSchema.Asserts(schema)
      const decoding = asserts.decoding()
      await decoding.fail(
        "a",
        `error message`
      )
    })

    describe("returns issue", () => {
      it("abort: false", async () => {
        const schema = Schema.String.check(
          Schema.makeFilter((s) => new SchemaIssue.InvalidValue(Option.some(s), { message: "error message 1" }), {
            title: "filter title 1"
          }),
          Schema.makeFilter(() => false, { title: "filter title 2", message: "error message 2" })
        )
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding({ parseOptions: { errors: "all" } })
        await decoding.fail(
          "a",
          `error message 1
error message 2`
        )
      })

      it("abort: true", async () => {
        const schema = Schema.String.check(
          Schema.makeFilter((s) => new SchemaIssue.InvalidValue(Option.some(s), { message: "error message 1" }), {
            title: "filter title 1"
          }, true),
          Schema.makeFilter(() => false, { title: "filter title 2", message: "error message 2" })
        )
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding({ parseOptions: { errors: "all" } })
        await decoding.fail(
          "a",
          `error message 1`
        )
      })
    })

    describe("returns object", () => {
      it("abort: false", async () => {
        const schema = Schema.String.check(
          Schema.makeFilter(() => ({
            path: ["a"],
            issue: "error message 1"
          }), { title: "filter title 1" }),
          Schema.makeFilter(() => false, { title: "filter title 2", message: "error message 2" })
        )
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding({ parseOptions: { errors: "all" } })
        await decoding.fail(
          "a",
          `error message 1
  at ["a"]
error message 2`
        )
      })

      it("abort: true", async () => {
        const schema = Schema.String.check(
          Schema.makeFilter(() => ({ path: ["a"], issue: "error message 1" }), { title: "error title 1" }, true),
          Schema.makeFilter(() => false, { title: "error title 2", message: "error message 2" })
        )
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding({ parseOptions: { errors: "all" } })
        await decoding.fail(
          "a",
          `error message 1
  at ["a"]`
        )
      })

      it("issue: Issue", async () => {
        const schema = Schema.String.check(
          Schema.makeFilter(
            (s) => ({ path: ["a"], issue: new SchemaIssue.InvalidValue(Option.some(s), { message: "custom issue" }) }),
            { title: "filter title" }
          )
        )
        const asserts = new TestSchema.Asserts(schema)

        const decoding = asserts.decoding()
        await decoding.fail(
          "a",
          `custom issue
  at ["a"]`
        )
      })
    })

    describe("returns array", () => {
      it("empty array is treated as success", async () => {
        const schema = Schema.String.check(Schema.makeFilter(() => []))
        const asserts = new TestSchema.Asserts(schema)
        const decoding = asserts.decoding()
        await decoding.succeed("a")
      })

      it("single-element array collapses to the element", async () => {
        const schema = Schema.String.check(
          Schema.makeFilter(() => [{ path: ["a"], issue: "error message 1" }], { title: "filter title" })
        )
        const asserts = new TestSchema.Asserts(schema)
        const decoding = asserts.decoding()
        await decoding.fail(
          "a",
          `error message 1
  at ["a"]`
        )
      })

      it("multi-element array groups into a Composite", async () => {
        const schema = Schema.String.check(
          Schema.makeFilter(() => [
            { path: ["a"], issue: "error message 1" },
            { path: ["b"], issue: "error message 2" }
          ], { title: "filter title" })
        )
        const asserts = new TestSchema.Asserts(schema)
        const decoding = asserts.decoding({ parseOptions: { errors: "all" } })
        await decoding.fail(
          "a",
          `error message 1
  at ["a"]
error message 2
  at ["b"]`
        )
      })

      it("array mixing string, Issue, and { path, issue }", async () => {
        const schema = Schema.String.check(
          Schema.makeFilter((s) => [
            "top-level message",
            new SchemaIssue.InvalidValue(Option.some(s), { message: "direct issue" }),
            { path: ["a"], issue: "pointed message" }
          ], { title: "filter title" })
        )
        const asserts = new TestSchema.Asserts(schema)
        const decoding = asserts.decoding({ parseOptions: { errors: "all" } })
        await decoding.fail(
          "a",
          `top-level message
direct issue
pointed message
  at ["a"]`
        )
      })
    })
  })

  describe("extendTo", () => {
    it("Struct", async () => {
      const schema = Schema.Struct({
        a: Schema.String,
        b: Schema.Number
      }).pipe(Schema.extendTo({
        c: Schema.String
      }, {
        c: (value) => Option.some(value.a + "c" + value.b)
      }))
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "1", b: 2 }, { a: "1", b: 2, c: "1c2" })

      const encoding = asserts.encoding()
      await encoding.succeed({ a: "1", b: 2, c: "1c2" }, { a: "1", b: 2 })
    })

    it("Union", async () => {
      const Circle = Schema.Struct({
        radius: Schema.Number
      })

      const Square = Schema.Struct({
        sideLength: Schema.Number
      })

      const DiscriminatedShape = Schema.Union([
        Circle.pipe(Schema.extendTo({ kind: Schema.tag("circle") }, { kind: () => Option.some("circle" as const) })),
        Square.pipe(Schema.extendTo({ kind: Schema.tag("square") }, { kind: () => Option.some("square" as const) }))
      ])
      const asserts = new TestSchema.Asserts(DiscriminatedShape)

      const decoding = asserts.decoding()
      await decoding.succeed({ radius: 1 }, { radius: 1, kind: "circle" })
      await decoding.succeed({ sideLength: 1 }, {
        sideLength: 1,
        kind: "square"
      })

      const encoding = asserts.encoding()
      await encoding.succeed({ radius: 1, kind: "circle" }, { radius: 1 })
      await encoding.succeed({ sideLength: 1, kind: "square" }, {
        sideLength: 1
      })
    })
  })

  describe("Tagged unions", () => {
    describe("toTaggedUnion", () => {
      it("should augment a union of structs", () => {
        const b = Symbol.for("B")
        const schema = Schema.Union([
          Schema.Struct({ _tag: Schema.Literal("A"), a: Schema.String }),
          Schema.Struct({ _tag: Schema.UniqueSymbol(b), b: Schema.FiniteFromString }),
          Schema.Union([
            Schema.Struct({ _tag: Schema.Literal(1), c: Schema.Boolean }),
            Schema.Struct({ _tag: Schema.Literal("D"), d: Schema.Date })
          ])
        ]).pipe(Schema.toTaggedUnion("_tag"))

        // cases
        deepStrictEqual(schema.cases.A, schema.members[0])
        deepStrictEqual(schema.cases[b], schema.members[1])
        deepStrictEqual(schema.cases[1], schema.members[2].members[0])
        deepStrictEqual(schema.cases["1"], schema.members[2].members[0])
        deepStrictEqual(schema.cases.D, schema.members[2].members[1])

        // isAnyOf
        const isAOr1 = schema.isAnyOf(["A", 1])
        assertTrue(isAOr1({ _tag: "A", a: "a" }))
        assertTrue(isAOr1({ _tag: 1, c: true }))
        assertFalse(isAOr1({ _tag: "D", d: new Date() }))
        assertFalse(isAOr1({ _tag: b, b: 1 }))

        // guards
        assertTrue(schema.guards.A({ _tag: "A", a: "a" }))
        assertFalse(schema.guards.A({ _tag: "A", a: 1 }))

        assertTrue(schema.guards[b]({ _tag: b, b: 1 }))
        assertFalse(schema.guards[b]({ _tag: b, b: "b" }))

        assertTrue(schema.guards[1]({ _tag: 1, c: true }))
        assertFalse(schema.guards[1]({ _tag: 1, c: 1 }))

        assertTrue(schema.guards.D({ _tag: "D", d: new Date() }))
        assertFalse(schema.guards.D({ _tag: "D", d: "d" }))

        // match
        deepStrictEqual(
          schema.match({ _tag: "A", a: "a" }, { A: () => "A", [b]: () => "B", 1: () => "C", D: () => "D" }),
          "A"
        )
        deepStrictEqual(
          pipe({ _tag: "A", a: "a" }, schema.match({ A: () => "A", [b]: () => "B", 1: () => "C", D: () => "D" })),
          "A"
        )
        deepStrictEqual(
          schema.match({ _tag: b, b: 1 }, { A: () => "A", [b]: () => "B", 1: () => "C", D: () => "D" }),
          "B"
        )
        deepStrictEqual(
          pipe({ _tag: b, b: 1 }, schema.match({ A: () => "A", [b]: () => "B", 1: () => "C", D: () => "D" })),
          "B"
        )
        deepStrictEqual(
          schema.match({ _tag: 1, c: true }, { A: () => "A", [b]: () => "B", 1: () => "C", D: () => "D" }),
          "C"
        )
        deepStrictEqual(
          pipe({ _tag: 1, c: true }, schema.match({ A: () => "A", [b]: () => "B", 1: () => "C", D: () => "D" })),
          "C"
        )
        deepStrictEqual(
          schema.match({ _tag: "D", d: new Date() }, { A: () => "A", [b]: () => "B", 1: () => "C", D: () => "D" }),
          "D"
        )
        deepStrictEqual(
          pipe(
            { _tag: "D", d: new Date() },
            schema.match({ A: () => "A", [b]: () => "B", 1: () => "C", D: () => "D" })
          ),
          "D"
        )
      })

      it("should support multiple tags", () => {
        const schema = Schema.Union([
          Schema.Struct({ _tag: Schema.tag("A"), type: Schema.tag("TypeA"), a: Schema.String }),
          Schema.Struct({ _tag: Schema.tag("B"), type: Schema.tag("TypeB"), b: Schema.FiniteFromString })
        ]).pipe(Schema.toTaggedUnion("type"))

        // cases
        deepStrictEqual(schema.cases.TypeA, schema.members[0])
        deepStrictEqual(schema.cases.TypeB, schema.members[1])
      })

      it("should augment a union of classes", () => {
        class A extends Schema.Class<A>("A")({
          _tag: Schema.tag("A"),
          a: Schema.String
        }) {}
        class B extends Schema.Class<B>("B")({
          _tag: Schema.tag("B"),
          b: Schema.FiniteFromString
        }) {}

        const schema = Schema.Union([A, B]).pipe(Schema.toTaggedUnion("_tag"))

        // cases
        deepStrictEqual(schema.cases.A, A)
        deepStrictEqual(schema.cases.B, B)
      })
    })

    describe("TaggedUnion", () => {
      it("should create a tagged union", () => {
        const schema = Schema.TaggedUnion({
          A: { a: Schema.String },
          C: { c: Schema.Boolean },
          B: { b: Schema.FiniteFromString }
        }).annotate({})

        const { A, B, C } = schema.cases

        // cases
        strictEqual(A.fields._tag.ast.literal, "A")
        strictEqual(A.fields.a, Schema.String)
        strictEqual(B.fields._tag.ast.literal, "B")
        strictEqual(B.fields.b, Schema.FiniteFromString)
        strictEqual(C.fields._tag.ast.literal, "C")
        strictEqual(C.fields.c, Schema.Boolean)

        // isAnyOf
        const isAOrB = schema.isAnyOf(["A", "B"])
        assertTrue(isAOrB({ _tag: "A", a: "a" }))
        assertTrue(isAOrB({ _tag: "B", b: 1 }))
        assertFalse(isAOrB({ _tag: "C", c: true }))

        // guards
        assertTrue(schema.guards.A({ _tag: "A", a: "a" }))
        assertTrue(schema.guards.B({ _tag: "B", b: 1 }))
        assertTrue(schema.guards.C({ _tag: "C", c: true }))
        assertFalse(schema.guards.A({ _tag: "A", b: 1 }))
        assertFalse(schema.guards.B({ _tag: "B", a: "a" }))
        assertFalse(schema.guards.C({ _tag: "C", c: 1 }))

        // match
        deepStrictEqual(
          schema.match({ _tag: "A", a: "a" }, { A: () => "A", B: () => "B", C: () => "C" }),
          "A"
        )
        deepStrictEqual(
          pipe({ _tag: "A", a: "a" }, schema.match({ A: () => "A", B: () => "B", C: () => "C" })),
          "A"
        )
        deepStrictEqual(
          schema.match({ _tag: "B", b: 1 }, { A: () => "A", B: () => "B", C: () => "C" }),
          "B"
        )
        deepStrictEqual(
          pipe({ _tag: "B", b: 1 }, schema.match({ A: () => "A", B: () => "B", C: () => "C" })),
          "B"
        )
        deepStrictEqual(
          schema.match({ _tag: "C", c: true }, { A: () => "A", B: () => "B", C: () => "C" }),
          "C"
        )
        deepStrictEqual(
          pipe({ _tag: "C", c: true }, schema.match({ A: () => "A", B: () => "B", C: () => "C" })),
          "C"
        )
      })
    })
  })

  describe("withDecodingDefaultKey", () => {
    it("should return a decoding default value if the key is missing", async () => {
      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultKey(Effect.succeed("1")))
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({}, { a: 1 })
      await decoding.succeed({ a: "2" }, { a: 2 })
      await decoding.fail(
        { a: undefined },
        `Expected string, got undefined
  at ["a"]`
      )
    })

    it("by default should pass through the value", async () => {
      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultKey(Effect.succeed("1")))
      })
      const asserts = new TestSchema.Asserts(schema)

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1 }, { a: "1" })
    })

    it("should omit the value if the encoding strategy is set to omit", async () => {
      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(
          Schema.withDecodingDefaultKey(Effect.succeed("1"), { encodingStrategy: "omit" })
        )
      })
      const asserts = new TestSchema.Asserts(schema)

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1 }, {})
    })

    it("nested default values", async () => {
      const schema = Schema.Struct({
        a: Schema.Struct({
          b: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultKey(Effect.succeed("1")))
        }).pipe(Schema.withDecodingDefaultKey(Effect.succeed({})))
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({}, { a: { b: 1 } })
      await decoding.succeed({ a: {} }, { a: { b: 1 } })
      await decoding.succeed({ a: { b: "2" } }, { a: { b: 2 } })
      await decoding.fail(
        { a: { b: undefined } },
        `Expected string, got undefined
  at ["a"]["b"]`
      )
    })

    it("Effect failing with SchemaError propagates as decode failure", async () => {
      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultKey(
          Effect.fail(
            new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.none(), { message: "decoding default failed" }))
          )
        ))
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "2" }, { a: 2 })
      await decoding.fail(
        {},
        `decoding default failed
  at ["a"]`
      )
    })

    it("Effect failing with SchemaError and a defect preserves the mixed cause", () => {
      const cause = Cause.combine(
        Cause.fail(
          new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.none(), { message: "decoding default failed" }))
        ),
        Cause.die(new Error("defect"))
      )
      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultKey(Effect.failCause(cause)))
      })

      const exit = Schema.decodeUnknownExit(schema)({})
      assertTrue(Exit.isFailure(exit))
      assertTrue(Exit.hasDies(exit))
      const error = Cause.findError(exit.cause)
      assertTrue(Result.isSuccess(error))
      assertTrue(Schema.isSchemaError(error.success))
      strictEqual(
        error.success.message,
        `decoding default failed
  at ["a"]`
      )
    })

    it("default Effect can require a service", async () => {
      class Service extends Context.Service<Service, { fallback: Effect.Effect<string> }>()("Service") {}

      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultKey(
          Effect.gen(function*() {
            const service = yield* Service
            return yield* service.fallback
          })
        ))
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding().provide(Service, { fallback: Effect.succeed("3") })
      await decoding.succeed({ a: "2" }, { a: 2 })
      await decoding.succeed({}, { a: 3 })
    })
  })

  describe("withDecodingDefault", () => {
    it("should return a decoding default value if the key is missing", async () => {
      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(Schema.withDecodingDefault(Effect.succeed("1")))
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({}, { a: 1 })
      await decoding.succeed({ a: undefined }, { a: 1 })
      await decoding.succeed({ a: "2" }, { a: 2 })
    })

    it("should return a decoding default value if the schema is used as standalone and the input is undefined", async () => {
      const schema = Schema.String.pipe(Schema.withDecodingDefault(Effect.succeed("a")))
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(undefined, "a")
      await decoding.succeed("b", "b")
    })

    it("by default should pass through the value", async () => {
      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(Schema.withDecodingDefault(Effect.succeed("1")))
      })
      const asserts = new TestSchema.Asserts(schema)

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1 }, { a: "1" })
    })

    it("should omit the value if the encoding strategy is set to omit", async () => {
      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(Schema.withDecodingDefault(Effect.succeed("1"), { encodingStrategy: "omit" }))
      })
      const asserts = new TestSchema.Asserts(schema)

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1 }, {})
    })

    it("nested default values", async () => {
      const schema = Schema.Struct({
        a: Schema.Struct({
          b: Schema.FiniteFromString.pipe(Schema.withDecodingDefault(Effect.succeed("1")))
        }).pipe(Schema.withDecodingDefault(Effect.succeed({})))
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({}, { a: { b: 1 } })
      await decoding.succeed({ a: {} }, { a: { b: 1 } })
      await decoding.succeed({ a: undefined }, { a: { b: 1 } })
      await decoding.succeed({ a: { b: undefined } }, { a: { b: 1 } })
      await decoding.succeed({ a: { b: "2" } }, { a: { b: 2 } })
    })

    it("Effect failing with SchemaError and a defect preserves the mixed cause", () => {
      const cause = Cause.combine(
        Cause.fail(
          new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.none(), { message: "decoding default failed" }))
        ),
        Cause.die(new Error("defect"))
      )
      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(Schema.withDecodingDefault(Effect.failCause(cause)))
      })

      const exit = Schema.decodeUnknownExit(schema)({})
      assertTrue(Exit.isFailure(exit))
      assertTrue(Exit.hasDies(exit))
      const error = Cause.findError(exit.cause)
      assertTrue(Result.isSuccess(error))
      assertTrue(Schema.isSchemaError(error.success))
      strictEqual(
        error.success.message,
        `decoding default failed
  at ["a"]`
      )
    })

    it("default Effect can require a service", async () => {
      class Service extends Context.Service<Service, { fallback: Effect.Effect<string> }>()("Service") {}

      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(Schema.withDecodingDefault(
          Effect.gen(function*() {
            const service = yield* Service
            return yield* service.fallback
          })
        ))
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding().provide(Service, { fallback: Effect.succeed("3") })
      await decoding.succeed({ a: "2" }, { a: 2 })
      await decoding.succeed({}, { a: 3 })
      await decoding.succeed({ a: undefined }, { a: 3 })
    })
  })

  describe("withDecodingDefaultTypeKey", () => {
    it("should return a decoding default value if the key is missing", async () => {
      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultTypeKey(Effect.succeed(1)))
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({}, { a: 1 })
      await decoding.succeed({ a: "2" }, { a: 2 })
      await decoding.fail(
        { a: undefined },
        `Expected string, got undefined
  at ["a"]`
      )
    })

    it("by default should pass through the value", async () => {
      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultTypeKey(Effect.succeed(1)))
      })
      const asserts = new TestSchema.Asserts(schema)

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1 }, { a: "1" })
    })

    it("should omit the value if the encoding strategy is set to omit", async () => {
      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(
          Schema.withDecodingDefaultTypeKey(Effect.succeed(1), { encodingStrategy: "omit" })
        )
      })
      const asserts = new TestSchema.Asserts(schema)

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1 }, {})
    })

    it("nested default values", async () => {
      const schema = Schema.Struct({
        a: Schema.Struct({
          b: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultTypeKey(Effect.succeed(1)))
        }).pipe(Schema.withDecodingDefaultTypeKey(Effect.succeed({ b: 1 })))
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({}, { a: { b: 1 } })
      await decoding.succeed({ a: {} }, { a: { b: 1 } })
      await decoding.succeed({ a: { b: "2" } }, { a: { b: 2 } })
      await decoding.fail(
        { a: { b: undefined } },
        `Expected string, got undefined
  at ["a"]["b"]`
      )
    })

    it("Effect failing with SchemaError propagates as decode failure", async () => {
      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultTypeKey(
          Effect.fail(
            new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.none(), { message: "decoding default failed" }))
          )
        ))
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "2" }, { a: 2 })
      await decoding.fail(
        {},
        `decoding default failed
  at ["a"]`
      )
    })

    it("default Effect can require a service", async () => {
      class Service extends Context.Service<Service, { fallback: Effect.Effect<number> }>()("Service") {}

      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultTypeKey(
          Effect.gen(function*() {
            const service = yield* Service
            return yield* service.fallback
          })
        ))
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding().provide(Service, { fallback: Effect.succeed(3) })
      await decoding.succeed({ a: "2" }, { a: 2 })
      await decoding.succeed({}, { a: 3 })
    })
  })

  describe("withDecodingDefaultType", () => {
    it("should return a decoding default value if the key is missing", async () => {
      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultType(Effect.succeed(1)))
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({}, { a: 1 })
      await decoding.succeed({ a: undefined }, { a: 1 })
      await decoding.succeed({ a: "2" }, { a: 2 })
    })

    it("should return a decoding default value if the schema is used as standalone and the input is undefined", async () => {
      const schema = Schema.String.pipe(Schema.withDecodingDefaultType(Effect.succeed("a")))
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(undefined, "a")
      await decoding.succeed("b", "b")
    })

    it("by default should pass through the value", async () => {
      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultType(Effect.succeed(1)))
      })
      const asserts = new TestSchema.Asserts(schema)

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1 }, { a: "1" })
    })

    it("should omit the value if the encoding strategy is set to omit", async () => {
      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(
          Schema.withDecodingDefaultType(Effect.succeed(1), { encodingStrategy: "omit" })
        )
      })
      const asserts = new TestSchema.Asserts(schema)

      const encoding = asserts.encoding()
      await encoding.succeed({ a: 1 }, {})
    })

    it("nested default values", async () => {
      const schema = Schema.Struct({
        a: Schema.Struct({
          b: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultType(Effect.succeed(1)))
        }).pipe(Schema.withDecodingDefaultType(Effect.succeed({ b: 1 })))
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({}, { a: { b: 1 } })
      await decoding.succeed({ a: {} }, { a: { b: 1 } })
      await decoding.succeed({ a: undefined }, { a: { b: 1 } })
      await decoding.succeed({ a: { b: undefined } }, { a: { b: 1 } })
      await decoding.succeed({ a: { b: "2" } }, { a: { b: 2 } })
    })

    it("Effect failing with SchemaError propagates as decode failure", async () => {
      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultType(
          Effect.fail(
            new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.none(), { message: "decoding default failed" }))
          )
        ))
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "2" }, { a: 2 })
      await decoding.fail(
        {},
        `decoding default failed
  at ["a"]`
      )
    })

    it("default Effect can require a service", async () => {
      class Service extends Context.Service<Service, { fallback: Effect.Effect<number> }>()("Service") {}

      const schema = Schema.Struct({
        a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultType(
          Effect.gen(function*() {
            const service = yield* Service
            return yield* service.fallback
          })
        ))
      })
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding().provide(Service, { fallback: Effect.succeed(3) })
      await decoding.succeed({ a: "2" }, { a: 2 })
      await decoding.succeed({}, { a: 3 })
      await decoding.succeed({ a: undefined }, { a: 3 })
    })
  })

  it("NonEmptyString", async () => {
    const schema = Schema.NonEmptyString
    const asserts = new TestSchema.Asserts(schema)

    const decoding = asserts.decoding()
    await decoding.succeed("a")
    await decoding.fail(
      "",
      `Expected a value with a length of at least 1, got ""`
    )

    const encoding = asserts.encoding()
    await encoding.succeed("a")
    await encoding.fail(
      "",
      `Expected a value with a length of at least 1, got ""`
    )
  })

  it("Char", async () => {
    const schema = Schema.Char
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed("a")
    await decoding.fail(
      "ab",
      `Expected a value with a length of 1, got "ab"`
    )

    const encoding = asserts.encoding()
    await encoding.succeed("a")
    await encoding.fail(
      "ab",
      `Expected a value with a length of 1, got "ab"`
    )
  })

  it("Int", async () => {
    const schema = Schema.Int
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed(1)
    await decoding.fail(
      1.1,
      `Expected an integer, got 1.1`
    )
    await decoding.fail(
      NaN,
      `Expected an integer, got NaN`
    )
    await decoding.fail(
      Infinity,
      `Expected an integer, got Infinity`
    )
    await decoding.fail(
      -Infinity,
      `Expected an integer, got -Infinity`
    )

    const encoding = asserts.encoding()
    await encoding.succeed(1)
    await encoding.fail(
      1.1,
      `Expected an integer, got 1.1`
    )
  })

  it("Capitalize", async () => {
    const schema = Schema.String.pipe(
      Schema.decodeTo(
        Schema.String.check(Schema.isCapitalized()),
        SchemaTransformation.capitalize()
      )
    )
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed("abc", "Abc")

    const encoding = asserts.encoding()
    await encoding.succeed("Abc")
    await encoding.fail(
      "abc",
      `Expected a string with the first character in uppercase, got "abc"`
    )
  })

  it("Uncapitalize", async () => {
    const schema = Schema.String.pipe(
      Schema.decodeTo(
        Schema.String.check(Schema.isUncapitalized()),
        SchemaTransformation.uncapitalize()
      )
    )
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed("Abc", "abc")

    const encoding = asserts.encoding()
    await encoding.succeed("abc")
    await encoding.fail(
      "Abc",
      `Expected a string with the first character in lowercase, got "Abc"`
    )
  })

  it("Lowercase", async () => {
    const schema = Schema.String.pipe(
      Schema.decodeTo(
        Schema.String.check(Schema.isLowercased()),
        SchemaTransformation.toLowerCase()
      )
    )
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed("ABC", "abc")

    const encoding = asserts.encoding()
    await encoding.succeed("abc")
    await encoding.fail(
      "ABC",
      `Expected a string with all characters in lowercase, got "ABC"`
    )
  })

  it("Uppercase", async () => {
    const schema = Schema.String.pipe(
      Schema.decodeTo(
        Schema.String.check(Schema.isUppercased()),
        SchemaTransformation.toUpperCase()
      )
    )
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    const decoding = asserts.decoding()
    await decoding.succeed("abc", "ABC")

    const encoding = asserts.encoding()
    await encoding.succeed("ABC")
    await encoding.fail(
      "abc",
      `Expected a string with all characters in uppercase, got "abc"`
    )
  })
})

describe("Getter", () => {
  it("succeed", async () => {
    const schema = Schema.Literal(0).pipe(Schema.decodeTo(Schema.Literal("a"), {
      decode: SchemaGetter.succeed("a"),
      encode: SchemaGetter.succeed(0)
    }))
    const asserts = new TestSchema.Asserts(schema)

    const decoding = asserts.decoding()
    await decoding.succeed(0, "a")
    await decoding.fail(1, `Expected 0, got 1`)

    const encoding = asserts.encoding()
    await encoding.succeed("a", 0)
    await encoding.fail("b", `Expected "a", got "b"`)
  })
})

describe("Check", () => {
  it("isStringFinite", async () => {
    const schema = Schema.String.check(Schema.isStringFinite())

    deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], {
      _tag: "isStringFinite",
      regExp: /^[+-]?\d*\.?\d+(?:[Ee][+-]?\d+)?$/
    })
  })

  it("isStringBigInt", async () => {
    const schema = Schema.String.check(Schema.isStringBigInt())

    deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], {
      _tag: "isStringBigInt",
      regExp: /^-?\d+$/
    })
  })

  it("isStringSymbol", async () => {
    const schema = Schema.String.check(Schema.isStringSymbol())

    deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], {
      _tag: "isStringSymbol",
      regExp: /^Symbol\((.*)\)$/
    })
  })

  it("isUUID", async () => {
    const schema = Schema.String.check(Schema.isUUID())
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], {
      _tag: "isUUID",
      regExp:
        /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|[fF]{8}-[fF]{4}-[fF]{4}-[fF]{4}-[fF]{12})$/,
      version: undefined
    })

    const decoding = asserts.decoding()
    await decoding.succeed("00000000-0000-0000-0000-000000000000")
    await decoding.succeed("ffffffff-ffff-ffff-ffff-ffffffffffff")
    await decoding.succeed("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF")
    await decoding.succeed("00000000-0000-4000-8000-000000000001")
    await decoding.fail(
      "00000000-0000-0000-0000-000000000001",
      `Expected a UUID, got "00000000-0000-0000-0000-000000000001"`
    )
  })

  it("isUUID version-specific checks reject nil and max UUIDs", async () => {
    const schema = Schema.String.check(Schema.isUUID(4))
    const asserts = new TestSchema.Asserts(schema)
    const decoding = asserts.decoding()

    await decoding.succeed("00000000-0000-4000-8000-000000000001")
    await decoding.fail(
      "00000000-0000-0000-0000-000000000000",
      `Expected a UUID v4, got "00000000-0000-0000-0000-000000000000"`
    )
    await decoding.fail(
      "ffffffff-ffff-ffff-ffff-ffffffffffff",
      `Expected a UUID v4, got "ffffffff-ffff-ffff-ffff-ffffffffffff"`
    )
  })

  it("isGUID", async () => {
    const schema = Schema.String.check(Schema.isGUID())
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], {
      _tag: "isGUID",
      regExp: /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/
    })

    const decoding = asserts.decoding()
    await decoding.succeed("00000000-0000-0000-0000-000000000001")
    await decoding.succeed("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF")
    await decoding.fail(
      "not-a-guid",
      `Expected a GUID, got "not-a-guid"`
    )
  })

  it("isULID", async () => {
    const schema = Schema.String.check(Schema.isULID())
    const asserts = new TestSchema.Asserts(schema)

    if (verifyGeneration) {
      asserts.arbitrary().verifyGeneration()
    }

    deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], {
      _tag: "isULID",
      regExp: /^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$/
    })

    const decoding = asserts.decoding()
    await decoding.succeed("01H4PGGGJVN2DKP2K1H7EH996V")
    await decoding.fail(
      "",
      `Expected a string matching the RegExp ^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$, got ""`
    )
  })

  it("isBase64", async () => {
    const schema = Schema.String.check(Schema.isBase64())

    deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], {
      _tag: "isBase64",
      regExp: /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/
    })
  })

  it("isBase64Url", async () => {
    const schema = Schema.String.check(Schema.isBase64Url())

    deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], {
      _tag: "isBase64Url",
      regExp: /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/
    })
  })

  describe("brand", () => {
    it("single brand", async () => {
      const schema = Schema.String.pipe(Schema.brand("Positive"))
      deepStrictEqual(schema.ast.annotations?.brands, ["Positive"])
    })

    it("double brand", async () => {
      const schema = Schema.String.pipe(Schema.brand("Positive"), Schema.brand("Int"))
      deepStrictEqual(schema.ast.annotations?.brands, ["Positive", "Int"])
    })

    it("override the default identifier", async () => {
      const schema = Schema.String.pipe(Schema.brand("Positive"), Schema.brand("Int")).annotate({ identifier: "MyInt" })
      deepStrictEqual(schema.ast.annotations?.brands, ["Positive", "Int"])
    })
  })

  describe("fromBrand", () => {
    it("nominal", async () => {
      const schema = Schema.String.pipe(Schema.fromBrand("a", Brand.nominal<string & Brand.Brand<"a">>()))
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed("a")
      await decoding.fail(1, `Expected string, got 1`)

      deepStrictEqual(schema.ast.annotations?.brands, ["a"])
    })

    it("single brand", async () => {
      type Int = number & Brand.Brand<"Int">
      const Int = Brand.check<Int>(Schema.isInt())
      const schema = Schema.Number.pipe(Schema.fromBrand("Int", Int))

      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(1)
      await decoding.fail("a", `Expected number, got "a"`)
      await decoding.fail(1.2, `Expected an integer, got 1.2`)

      deepStrictEqual(schema.ast.checks?.at(-1)?.annotations?.brands, ["Int"])
    })

    it("multiple brands", async () => {
      type Int = number & Brand.Brand<"Int">
      const Int = Brand.check<Int>(Schema.isInt())

      type Positive = number & Brand.Brand<"Positive">
      const Positive = Brand.check<Positive>(Schema.isGreaterThan(0))

      const PositiveInt = Brand.all(Int, Positive)
      const schema = Schema.Number.pipe(Schema.fromBrand("PositiveInt", PositiveInt))

      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed(1)
      await decoding.fail("a", `Expected number, got "a"`)
      await decoding.fail(1.2, `Expected an integer, got 1.2`)
      await decoding.fail(-1, `Expected a value greater than 0, got -1`)

      deepStrictEqual(schema.ast.checks?.at(-1)?.annotations?.brands, ["PositiveInt"])
    })
  })

  describe("requiredKey", () => {
    it("should make all optionalKey keys required", async () => {
      const schema = Schema.Struct({
        a: Schema.optionalKey(Schema.String),
        b: Schema.optionalKey(Schema.Number)
      }).mapFields(Struct.map(Schema.requiredKey))
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "a", b: 1 })
      await decoding.fail(
        { a: "a" },
        `Missing key
  at ["b"]`
      )
      await decoding.fail(
        { a: "a", b: undefined },
        `Expected number, got undefined
  at ["b"]`
      )
      await decoding.fail(
        { b: 1 },
        `Missing key
  at ["a"]`
      )
    })

    it("should make all optional keys required", async () => {
      const schema = Schema.Struct({
        a: Schema.optional(Schema.String),
        b: Schema.optional(Schema.Number)
      }).mapFields(Struct.map(Schema.requiredKey))
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "a", b: 1 })
      await decoding.succeed({ a: "a", b: undefined })
      await decoding.succeed({ a: undefined, b: 1 })
      await decoding.succeed({ a: undefined, b: undefined })
      await decoding.fail(
        { a: "a" },
        `Missing key
  at ["b"]`
      )
      await decoding.fail(
        { b: 1 },
        `Missing key
  at ["a"]`
      )
    })
  })

  describe("required", () => {
    it("should make all optional keys required", async () => {
      const schema = Schema.Struct({
        a: Schema.optional(Schema.String),
        b: Schema.optional(Schema.Number)
      }).mapFields(Struct.map(Schema.required))
      const asserts = new TestSchema.Asserts(schema)

      const decoding = asserts.decoding()
      await decoding.succeed({ a: "a", b: 1 })
      await decoding.fail(
        { a: "a" },
        `Missing key
  at ["b"]`
      )
      await decoding.fail(
        { a: "a", b: undefined },
        `Expected number, got undefined
  at ["b"]`
      )
      await decoding.fail(
        { b: 1 },
        `Missing key
  at ["a"]`
      )
      await decoding.fail(
        { a: undefined, b: 1 },
        `Expected string, got undefined
  at ["a"]`
      )
    })
  })

  it("fieldsAssign", async () => {
    const schema = Schema.Union([
      Schema.Struct({
        a: Schema.String
      }),
      Schema.Struct({
        b: Schema.Number
      })
    ]).mapMembers(Tuple.map(Schema.fieldsAssign({ c: Schema.Number })))
    const asserts = new TestSchema.Asserts(schema)

    const decoding = asserts.decoding()
    await decoding.succeed({ a: "a", c: 1 })
    await decoding.succeed({ b: 1, c: 1 })
    await decoding.fail(
      { a: "a" },
      `Missing key
  at ["c"]
Missing key
  at ["b"]`
    )
    await decoding.fail(
      { b: 1 },
      `Missing key
  at ["a"]
Missing key
  at ["c"]`
    )
  })

  describe("asClass", () => {
    it("wrapping a primitive schema", () => {
      class A extends Schema.asClass(Schema.String) {}

      strictEqual(Schema.decodeUnknownSync(A)("a"), "a")
    })

    it("static getter using this", () => {
      class A extends Schema.asClass(Schema.String) {
        static get decodeUnknownSync() {
          return Schema.decodeUnknownSync(this)
        }
      }

      strictEqual(A.decodeUnknownSync("a"), "a")
    })

    it("static property", () => {
      class A extends Schema.asClass(Schema.String) {
        static readonly decodeUnknownSync = Schema.decodeUnknownSync(this)
      }

      strictEqual(A.decodeUnknownSync("a"), "a")
    })

    it("static property using Schema.suspend", () => {
      class A extends Schema.asClass(Schema.String) {
        static readonly decodeUnknownSync = Schema.decodeUnknownSync(Schema.suspend(() => this))
      }

      strictEqual(A.decodeUnknownSync("a"), "a")
    })

    it("wrapping a Struct schema", () => {
      const struct = Schema.Struct({
        name: Schema.String
      })
      class A extends Schema.asClass(struct) {
        static get decodeUnknownSync() {
          return Schema.decodeUnknownSync(this)
        }
      }

      deepStrictEqual(A.decodeUnknownSync({ name: "a" }), { name: "a" })
      strictEqual(A.fields, struct.fields)
    })

    it("subclassing (double wrap)", () => {
      class A extends Schema.asClass(Schema.FiniteFromString) {
        static get decodeUnknownSync() {
          return Schema.decodeUnknownSync(this)
        }
      }

      class B extends A {
        static encodeSync = Schema.encodeSync(this)
      }

      strictEqual(B.decodeUnknownSync("1"), 1)
      strictEqual(B.encodeSync(1), "1")
    })
  })
})

describe("resolveAnnotations", () => {
  it("returns undefined for a schema without annotations", () => {
    strictEqual(Schema.resolveAnnotations(Schema.String), undefined)
  })

  it("returns annotations from the base schema", () => {
    const schema = Schema.String.annotate({ title: "my string" })
    deepStrictEqual(Schema.resolveAnnotations(schema), { title: "my string" })
  })

  it("returns annotations from the last check", () => {
    const schema = Schema.String
      .annotate({ title: "base" })
      .check(Schema.isNonEmpty().annotate({ title: "check" }))
    strictEqual(Schema.resolveAnnotations(schema)?.title, "check")
  })
})

describe("resolveAnnotationsKey", () => {
  it("returns undefined for a schema without key annotations", () => {
    strictEqual(Schema.resolveAnnotationsKey(Schema.String), undefined)
  })

  it("returns key annotations", () => {
    const schema = Schema.String.annotateKey({ messageMissingKey: "required" })
    deepStrictEqual(Schema.resolveAnnotationsKey(schema), { messageMissingKey: "required" })
  })
})
