import { Redacted, Schema, SchemaRepresentation } from "effect"
import { describe, it } from "vitest"
import { deepStrictEqual, strictEqual } from "../../utils/assert.ts"

describe("toSchema", () => {
  function assertToSchemaRoundtrip(input: {
    schema: Schema.Top
    readonly reviver?: SchemaRepresentation.Reviver<Schema.Top> | undefined
  }, runtime: string) {
    const document = SchemaRepresentation.fromAST(input.schema.ast)
    const roundtrip = SchemaRepresentation.fromAST(
      SchemaRepresentation.toSchema(document, { reviver: input.reviver }).ast
    )
    deepStrictEqual(roundtrip, document)
    const codeDocument = SchemaRepresentation.toCodeDocument(SchemaRepresentation.toMultiDocument(roundtrip))
    strictEqual(codeDocument.codes[0].runtime, runtime)
  }

  describe("String", () => {
    it("String", () => {
      assertToSchemaRoundtrip(
        { schema: Schema.String },
        `Schema.String`
      )
    })

    it("String & check", () => {
      assertToSchemaRoundtrip(
        { schema: Schema.String.check(Schema.isMinLength(1)) },
        `Schema.String.check(Schema.isMinLength(1))`
      )
    })

    describe("checks", () => {
      it("isTrimmed", () => {
        assertToSchemaRoundtrip(
          { schema: Schema.String.check(Schema.isTrimmed()) },
          `Schema.String.check(Schema.isTrimmed())`
        )
      })

      it("isULID", () => {
        assertToSchemaRoundtrip(
          { schema: Schema.String.check(Schema.isULID()) },
          `Schema.String.check(Schema.isULID())`
        )
      })

      it("isGUID", () => {
        assertToSchemaRoundtrip(
          { schema: Schema.String.check(Schema.isGUID()) },
          `Schema.String.check(Schema.isGUID())`
        )
      })
    })
  })

  it("Struct", () => {
    assertToSchemaRoundtrip(
      { schema: Schema.Struct({}) },
      `Schema.Struct({  })`
    )
    assertToSchemaRoundtrip(
      { schema: Schema.Struct({ a: Schema.String }) },
      `Schema.Struct({ "a": Schema.String })`
    )
    assertToSchemaRoundtrip(
      { schema: Schema.Struct({ [Symbol.for("a")]: Schema.String }) },
      `Schema.Struct({ [_symbol]: Schema.String })`
    )
    assertToSchemaRoundtrip(
      { schema: Schema.Struct({ a: Schema.optionalKey(Schema.String) }) },
      `Schema.Struct({ "a": Schema.optionalKey(Schema.String) })`
    )
    assertToSchemaRoundtrip(
      { schema: Schema.Struct({ a: Schema.mutableKey(Schema.String) }) },
      `Schema.Struct({ "a": Schema.mutableKey(Schema.String) })`
    )
    assertToSchemaRoundtrip(
      { schema: Schema.Struct({ a: Schema.optionalKey(Schema.mutableKey(Schema.String)) }) },
      `Schema.Struct({ "a": Schema.optionalKey(Schema.mutableKey(Schema.String)) })`
    )
  })

  it("Record", () => {
    assertToSchemaRoundtrip(
      { schema: Schema.Record(Schema.String, Schema.Number) },
      `Schema.Record(Schema.String, Schema.Number)`
    )
    assertToSchemaRoundtrip(
      { schema: Schema.Record(Schema.Symbol, Schema.Number) },
      `Schema.Record(Schema.Symbol, Schema.Number)`
    )
  })

  it("StructWithRest", () => {
    assertToSchemaRoundtrip(
      {
        schema: Schema.StructWithRest(Schema.Struct({ a: Schema.Number }), [
          Schema.Record(Schema.String, Schema.Number)
        ])
      },
      `Schema.StructWithRest(Schema.Struct({ "a": Schema.Number }), [Schema.Record(Schema.String, Schema.Number)])`
    )
  })

  it("Tuple", () => {
    assertToSchemaRoundtrip(
      { schema: Schema.Tuple([]) },
      `Schema.Tuple([])`
    )
    assertToSchemaRoundtrip(
      { schema: Schema.Tuple([Schema.String, Schema.Number]) },
      `Schema.Tuple([Schema.String, Schema.Number])`
    )
    assertToSchemaRoundtrip(
      { schema: Schema.Tuple([Schema.String, Schema.optionalKey(Schema.Number)]) },
      `Schema.Tuple([Schema.String, Schema.optionalKey(Schema.Number)])`
    )
  })

  it("Array", () => {
    assertToSchemaRoundtrip(
      { schema: Schema.Array(Schema.String) },
      `Schema.Array(Schema.String)`
    )
  })

  it("TupleWithRest", () => {
    assertToSchemaRoundtrip(
      { schema: Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number]) },
      `Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number])`
    )
    assertToSchemaRoundtrip(
      { schema: Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number, Schema.Boolean]) },
      `Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number, Schema.Boolean])`
    )
  })

  it("Suspend", () => {
    type Category = {
      readonly name: string
      readonly children: ReadonlyArray<Category>
    }

    const OuterCategory = Schema.Struct({
      name: Schema.String,
      children: Schema.Array(Schema.suspend((): Schema.Codec<Category> => OuterCategory))
    }).annotate({ identifier: "Category" })

    assertToSchemaRoundtrip(
      { schema: OuterCategory },
      `Category`
    )
  })

  describe("brand", () => {
    it("brand", () => {
      assertToSchemaRoundtrip(
        { schema: Schema.String.pipe(Schema.brand("a")) },
        `Schema.String.pipe(Schema.brand("a"))`
      )
    })

    it("brand & brand", () => {
      assertToSchemaRoundtrip(
        { schema: Schema.String.pipe(Schema.brand("a"), Schema.brand("b")) },
        `Schema.String.pipe(Schema.brand("a"), Schema.brand("b"))`
      )
    })

    it("check & brand", () => {
      assertToSchemaRoundtrip(
        { schema: Schema.String.check(Schema.isMinLength(1)).pipe(Schema.brand("b")) },
        `Schema.String.check(Schema.isMinLength(1)).pipe(Schema.brand("b"))`
      )
    })

    it("brand & check & brand", () => {
      assertToSchemaRoundtrip(
        { schema: Schema.String.pipe(Schema.brand("a")).check(Schema.isMinLength(1)).pipe(Schema.brand("b")) },
        `Schema.String.pipe(Schema.brand("a")).check(Schema.isMinLength(1)).pipe(Schema.brand("b"))`
      )
    })

    it("check & brand & check", () => {
      assertToSchemaRoundtrip(
        { schema: Schema.String.check(Schema.isMinLength(1)).pipe(Schema.brand("b")).check(Schema.isMaxLength(2)) },
        `Schema.String.check(Schema.isMinLength(1)).pipe(Schema.brand("b")).check(Schema.isMaxLength(2))`
      )
    })
  })

  describe("toSchemaDefaultReviver", () => {
    function assertToSchemaWithReviver(schema: Schema.Top, runtime: string) {
      assertToSchemaRoundtrip({ schema, reviver: SchemaRepresentation.toSchemaDefaultReviver }, runtime)
    }

    it("Option", () => {
      assertToSchemaWithReviver(
        Schema.Option(Schema.String),
        `Schema.Option(Schema.String)`
      )
      assertToSchemaWithReviver(
        Schema.Option(Schema.URL),
        `Schema.Option(Schema.URL)`
      )
    })

    it("Result", () => {
      assertToSchemaWithReviver(
        Schema.Result(Schema.String, Schema.Number),
        `Schema.Result(Schema.String, Schema.Number)`
      )
    })

    it("Json", () => {
      assertToSchemaWithReviver(
        Schema.Json,
        `Schema.Json`
      )
    })

    it("MutableJson", () => {
      assertToSchemaWithReviver(
        Schema.MutableJson,
        `Schema.MutableJson`
      )
    })

    it("Redacted", () => {
      assertToSchemaWithReviver(
        Schema.Redacted(Schema.String),
        `Schema.Redacted(Schema.String)`
      )
    })

    it("Redacted options", () => {
      const schema = Schema.Redacted(Schema.String, {
        label: "password",
        disallowJsonEncode: true
      })
      const document = SchemaRepresentation.fromAST(schema.ast)
      const roundtrip = SchemaRepresentation.toSchema<typeof schema>(document, {
        reviver: SchemaRepresentation.toSchemaDefaultReviver
      })
      const encode = Schema.encodeUnknownExit(Schema.toCodecJson(roundtrip))

      strictEqual(
        String(encode(Redacted.make("secret", { label: "password" }))),
        `Failure(Cause([Fail(SchemaError(Cannot serialize Redacted with label: "password"))]))`
      )
      strictEqual(
        String(encode(Redacted.make("secret", { label: "other" }))),
        `Failure(Cause([Fail(SchemaError(Expected "password", got "other"
  at ["label"]))]))`
      )
    })

    it("CauseReason", () => {
      assertToSchemaWithReviver(
        Schema.CauseReason(Schema.String, Schema.Number),
        `Schema.CauseReason(Schema.String, Schema.Number)`
      )
    })

    it("Cause", () => {
      assertToSchemaWithReviver(
        Schema.Cause(Schema.String, Schema.Number),
        `Schema.Cause(Schema.String, Schema.Number)`
      )
    })

    it("Exit", () => {
      assertToSchemaWithReviver(
        Schema.Exit(Schema.String, Schema.Number, Schema.Boolean),
        `Schema.Exit(Schema.String, Schema.Number, Schema.Boolean)`
      )
    })

    it("ReadonlyMap", () => {
      assertToSchemaWithReviver(
        Schema.ReadonlyMap(Schema.String, Schema.Number),
        `Schema.ReadonlyMap(Schema.String, Schema.Number)`
      )
    })

    it("HashMap", () => {
      assertToSchemaWithReviver(
        Schema.HashMap(Schema.String, Schema.Number),
        `Schema.HashMap(Schema.String, Schema.Number)`
      )
    })

    it("Chunk", () => {
      assertToSchemaWithReviver(
        Schema.Chunk(Schema.String),
        `Schema.Chunk(Schema.String)`
      )
    })

    it("ReadonlySet", () => {
      assertToSchemaWithReviver(
        Schema.ReadonlySet(Schema.String),
        `Schema.ReadonlySet(Schema.String)`
      )
    })

    it("RegExp", () => {
      assertToSchemaWithReviver(
        Schema.RegExp,
        `Schema.RegExp`
      )
    })

    it("URL", () => {
      assertToSchemaWithReviver(
        Schema.URL,
        `Schema.URL`
      )
    })

    describe("Date", () => {
      it("Date", () => {
        assertToSchemaWithReviver(
          Schema.Date,
          `Schema.Date`
        )
      })

      describe("checks", () => {
        it("isDateValid", () => {
          assertToSchemaWithReviver(
            Schema.Date.check(Schema.isDateValid()),
            `Schema.Date.check(Schema.isDateValid())`
          )
        })

        it("isGreaterThanDate", () => {
          assertToSchemaWithReviver(
            Schema.Date.check(Schema.isGreaterThanDate(new Date(0))),
            `Schema.Date.check(Schema.isGreaterThanDate(new Date(0)))`
          )
        })

        it("isGreaterThanOrEqualToDate", () => {
          assertToSchemaWithReviver(
            Schema.Date.check(Schema.isGreaterThanOrEqualToDate(new Date(0))),
            `Schema.Date.check(Schema.isGreaterThanOrEqualToDate(new Date(0)))`
          )
        })

        it("isLessThanDate", () => {
          assertToSchemaWithReviver(
            Schema.Date.check(Schema.isLessThanDate(new Date(0))),
            `Schema.Date.check(Schema.isLessThanDate(new Date(0)))`
          )
        })

        it("isLessThanOrEqualToDate", () => {
          assertToSchemaWithReviver(
            Schema.Date.check(Schema.isLessThanOrEqualToDate(new Date(0))),
            `Schema.Date.check(Schema.isLessThanOrEqualToDate(new Date(0)))`
          )
        })
      })
    })

    it("Duration", () => {
      assertToSchemaWithReviver(
        Schema.Duration,
        `Schema.Duration`
      )
    })

    it("FormData", () => {
      assertToSchemaWithReviver(
        Schema.FormData,
        `Schema.FormData`
      )
    })

    it("URLSearchParams", () => {
      assertToSchemaWithReviver(
        Schema.URLSearchParams,
        `Schema.URLSearchParams`
      )
    })

    it("Uint8Array", () => {
      assertToSchemaWithReviver(
        Schema.Uint8Array,
        `Schema.Uint8Array`
      )
    })

    it("DateTime.Utc", () => {
      assertToSchemaWithReviver(
        Schema.DateTimeUtc,
        `Schema.DateTimeUtc`
      )
    })

    it("Error", () => {
      assertToSchemaWithReviver(
        Schema.Error(),
        `Schema.Error()`
      )
    })

    it("Error with stack", () => {
      assertToSchemaWithReviver(
        Schema.Error({ includeStack: true }),
        `Schema.Error({"includeStack":true})`
      )
    })

    it("Error with excluded cause", () => {
      assertToSchemaWithReviver(
        Schema.Error({ excludeCause: true }),
        `Schema.Error({"excludeCause":true})`
      )
    })

    it("Defect", () => {
      assertToSchemaWithReviver(
        Schema.Defect(),
        `Schema.Json`
      )
    })

    it("HashSet", () => {
      assertToSchemaWithReviver(
        Schema.HashSet(Schema.String),
        `Schema.HashSet(Schema.String)`
      )
    })

    it("BigDecimal", () => {
      assertToSchemaWithReviver(
        Schema.BigDecimal,
        `Schema.BigDecimal`
      )
    })

    it("TimeZoneOffset", () => {
      assertToSchemaWithReviver(
        Schema.TimeZoneOffset,
        `Schema.TimeZoneOffset`
      )
    })

    it("TimeZoneNamed", () => {
      assertToSchemaWithReviver(
        Schema.TimeZoneNamed,
        `Schema.TimeZoneNamed`
      )
    })

    it("TimeZone", () => {
      assertToSchemaWithReviver(
        Schema.TimeZone,
        `Schema.TimeZone`
      )
    })

    it("DateTimeZoned", () => {
      assertToSchemaWithReviver(
        Schema.DateTimeZoned,
        `Schema.DateTimeZoned`
      )
    })

    describe("ReadonlySet", () => {
      it("ReadonlySet(String)", () => {
        assertToSchemaWithReviver(
          Schema.ReadonlySet(Schema.String),
          `Schema.ReadonlySet(Schema.String)`
        )
      })

      describe("checks", () => {
        it("isMinSize", () => {
          assertToSchemaWithReviver(
            Schema.ReadonlySet(Schema.String).check(Schema.isMinSize(2)),
            `Schema.ReadonlySet(Schema.String).check(Schema.isMinSize(2))`
          )
        })

        it("isMaxSize", () => {
          assertToSchemaWithReviver(
            Schema.ReadonlySet(Schema.String).check(Schema.isMaxSize(2)),
            `Schema.ReadonlySet(Schema.String).check(Schema.isMaxSize(2))`
          )
        })
      })

      it("isSizeBetween", () => {
        assertToSchemaWithReviver(
          Schema.ReadonlySet(Schema.String).check(Schema.isSizeBetween(2, 2)),
          `Schema.ReadonlySet(Schema.String).check(Schema.isSizeBetween(2, 2))`
        )
      })
    })
  })
})
