import { assert, describe, it } from "@effect/vitest"
import { type JsonSchema, Schema } from "effect"
import { TestSchema } from "effect/testing"
import { toCodecOpenAI } from "effect/unstable/ai/OpenAiStructuredOutput"
import * as Tool from "effect/unstable/ai/Tool"

function assertJsonSchema(schema: Schema.Constraint, expected: JsonSchema.JsonSchema) {
  assert.deepStrictEqual(toCodecOpenAI(schema).jsonSchema, expected)
}

function assertError(schema: Schema.Constraint, message: string) {
  assert.throws(() => toCodecOpenAI(schema), message)
}

describe("toCodecOpenAI", () => {
  describe("Unsupported", () => {
    it("Undefined", () => {
      assertError(Schema.Undefined, "Unsupported AST Undefined")
    })

    it("Literal with unsupported type", () => {
      assertError(Schema.Literal(1n), "Unsupported literal type bigint")
    })

    describe("Arrays", () => {
      it("post-rest elements", () => {
        assertError(
          Schema.TupleWithRest(Schema.Tuple([]), [Schema.String, Schema.String]),
          "Post-rest elements are not supported for arrays"
        )
      })
    })

    describe("Objects", () => {
      it("non-string property signature name", () => {
        assertError(
          Schema.Struct({ [Symbol.for("effect/Schema/test/a")]: Schema.String }),
          "Property names must be strings"
        )
      })
    })
  })

  describe("Suspend", () => {
    it("no-transformation recursive schema", () => {
      interface A {
        readonly a: string
        readonly as: ReadonlyArray<A>
      }
      const schema = Schema.Struct({
        a: Schema.String.check(Schema.isStartsWith("a")),
        as: Schema.Array(Schema.suspend((): Schema.Codec<A> => schema))
      })
      assertJsonSchema(schema, {
        "type": "object",
        "properties": {
          "a": {
            "type": "string",
            "description": "a string starting with \"a\"",
            "pattern": "^a"
          },
          "as": {
            "type": "array",
            "items": { "$ref": "#/$defs/Suspend_" }
          }
        },
        "required": ["a", "as"],
        "additionalProperties": false,
        "$defs": {
          "Suspend_": {
            "type": "object",
            "properties": {
              "a": {
                "type": "string",
                "description": "a string starting with \"a\"",
                "pattern": "^a"
              },
              "as": {
                "type": "array",
                "items": { "$ref": "#/$defs/Suspend_" }
              }
            },
            "required": ["a", "as"],
            "additionalProperties": false
          }
        }
      })
    })

    it("transformation recursive schema", () => {
      interface A {
        readonly a: string
        readonly as: readonly [A]
      }
      const schema = Schema.Struct({
        a: Schema.String.check(Schema.isStartsWith("a")),
        as: Schema.Tuple([Schema.suspend((): Schema.Codec<A> => schema)])
      })
      assertJsonSchema(schema, {
        "type": "object",
        "properties": {
          "a": {
            "type": "string",
            "description": "a string starting with \"a\"",
            "pattern": "^a"
          },
          "as": {
            "type": "object",
            "description":
              "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements",
            "properties": {
              "0": {
                "$ref": "#/$defs/Suspend_"
              }
            },
            "required": ["0"],
            "additionalProperties": false
          }
        },
        "required": ["a", "as"],
        "additionalProperties": false,
        "$defs": {
          "Suspend_": {
            "type": "object",
            "properties": {
              "a": {
                "type": "string",
                "description": "a string starting with \"a\"",
                "pattern": "^a"
              },
              "as": {
                "type": "object",
                "description":
                  "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements",
                "properties": {
                  "0": {
                    "$ref": "#/$defs/Suspend_"
                  }
                },
                "required": ["0"],
                "additionalProperties": false
              }
            },
            "required": ["a", "as"],
            "additionalProperties": false
          }
        }
      })
    })
  })

  it("Null", () => {
    assertJsonSchema(Schema.Null, {
      "type": "null"
    })
  })

  describe("String", () => {
    it("String", () => {
      assertJsonSchema(Schema.String, {
        "type": "string"
      })
      assertJsonSchema(Schema.String.annotate({ description: "description" }), {
        "type": "string",
        "description": "description"
      })
    })

    it("String + supported format", () => {
      assertJsonSchema(Schema.String.annotate({ format: "date-time" }), {
        "type": "string",
        "format": "date-time"
      })
    })

    it("String + unsupported format", () => {
      assertJsonSchema(Schema.String.annotate({ format: "int32" }), {
        "type": "string",
        "description": "a value with a format of int32"
      })
    })

    it("String + isMinLength", () => {
      assertJsonSchema(Schema.String.check(Schema.isMinLength(1)), {
        "type": "string",
        "description": "a value with a length of at least 1"
      })
    })

    it("String + isMinLength + isMaxLength", () => {
      assertJsonSchema(Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(2)), {
        "type": "string",
        "description": "a value with a length of at least 1 and a value with a length of at most 2"
      })
    })

    it("String + startsWith", () => {
      assertJsonSchema(Schema.String.check(Schema.isStartsWith("a")), {
        "type": "string",
        "description": `a string starting with "a"`,
        "pattern": "^a"
      })
    })

    it("String + startsWith + endsWith", () => {
      assertJsonSchema(Schema.String.check(Schema.isStartsWith("a"), Schema.isEndsWith("b")), {
        "type": "string",
        "description": `a string starting with "a" and a string ending with "b"`,
        "pattern": "^(?=[\\s\\S]*?(?:^a))(?=[\\s\\S]*?(?:b$))"
      })
    })

    it("String + pattern", () => {
      assertJsonSchema(Schema.String.check(Schema.isPattern(/^a/)), {
        "type": "string",
        "description": `a string matching the RegExp ^a`,
        "pattern": "^a"
      })
    })
  })

  describe("Number", () => {
    it("Number", () => {
      assertJsonSchema(Schema.Number, {
        "anyOf": [
          { "type": "number" },
          { "type": "string", "enum": ["NaN"] },
          { "type": "string", "enum": ["Infinity"] },
          { "type": "string", "enum": ["-Infinity"] }
        ]
      })
    })

    describe("Finite", () => {
      it("Finite", () => {
        assertJsonSchema(Schema.Finite, {
          "type": "number"
        })
        assertJsonSchema(Schema.Finite.annotate({ description: "description" }), {
          "type": "number",
          "description": "description"
        })
      })

      it("Finite + supported format", () => {
        assertJsonSchema(Schema.Finite.annotate({ format: "duration" }), {
          "type": "number",
          "format": "duration"
        })
      })

      it("Finite + unsupported format", () => {
        assertJsonSchema(Schema.Finite.annotate({ format: "int32" }), {
          "type": "number",
          "description": "a value with a format of int32"
        })
      })

      it("Finite + isGreaterThan", () => {
        assertJsonSchema(Schema.Finite.check(Schema.isGreaterThan(1)), {
          "type": "number",
          "description": "a value greater than 1",
          "exclusiveMinimum": 1
        })
      })

      it("Finite + isGreaterThanOrEqualTo", () => {
        assertJsonSchema(Schema.Finite.check(Schema.isGreaterThanOrEqualTo(1)), {
          "type": "number",
          "description": "a value greater than or equal to 1",
          "minimum": 1
        })
      })

      it("Finite + isLessThan", () => {
        assertJsonSchema(Schema.Finite.check(Schema.isLessThan(2)), {
          "type": "number",
          "description": "a value less than 2",
          "exclusiveMaximum": 2
        })
      })

      it("Finite + isLessThanOrEqualTo", () => {
        assertJsonSchema(Schema.Finite.check(Schema.isLessThanOrEqualTo(2)), {
          "type": "number",
          "description": "a value less than or equal to 2",
          "maximum": 2
        })
      })

      it("Finite + isBetween", () => {
        assertJsonSchema(Schema.Finite.check(Schema.isBetween({ minimum: 1, maximum: 2 })), {
          "type": "number",
          "description": "a value between 1 and 2",
          "minimum": 1,
          "maximum": 2
        })
      })

      it("Finite + isMultipleOf", () => {
        assertJsonSchema(Schema.Finite.check(Schema.isMultipleOf(2)), {
          "type": "number",
          "description": "a value that is a multiple of 2",
          "multipleOf": 2
        })
      })

      it("Finite + isGreaterThan + isLessThan", () => {
        assertJsonSchema(Schema.Finite.check(Schema.isGreaterThan(1), Schema.isLessThan(2)), {
          "type": "number",
          "description": "a value greater than 1 and a value less than 2",
          "exclusiveMinimum": 1,
          "exclusiveMaximum": 2
        })
      })
    })

    describe("Int", () => {
      it("Int", () => {
        assertJsonSchema(Schema.Int, {
          "type": "integer"
        })
        assertJsonSchema(Schema.Int.annotate({ description: "description" }), {
          "type": "integer",
          "description": "description"
        })
      })

      it("Int + supported format", () => {
        assertJsonSchema(Schema.Int.annotate({ format: "duration" }), {
          "type": "integer",
          "format": "duration"
        })
      })

      it("Int + unsupported format", () => {
        assertJsonSchema(Schema.Int.annotate({ format: "int32" }), {
          "type": "integer",
          "description": "a value with a format of int32"
        })
      })

      it("Int + isGreaterThan", () => {
        assertJsonSchema(Schema.Int.check(Schema.isGreaterThan(1)), {
          "type": "integer",
          "description": "a value greater than 1",
          "exclusiveMinimum": 1
        })
      })

      it("Int + isGreaterThan + isLessThan", () => {
        assertJsonSchema(Schema.Int.check(Schema.isGreaterThan(1), Schema.isLessThan(2)), {
          "type": "integer",
          "description": "a value greater than 1 and a value less than 2",
          "exclusiveMinimum": 1,
          "exclusiveMaximum": 2
        })
      })
    })
  })

  describe("Literal", () => {
    it("Literal(string)", () => {
      assertJsonSchema(Schema.Literal("a"), {
        "type": "string",
        "enum": ["a"]
      })
    })

    it("Literal(number)", () => {
      assertJsonSchema(Schema.Literal(1), {
        "type": "number",
        "enum": [1]
      })
    })

    it("Literal(boolean)", () => {
      assertJsonSchema(Schema.Literal(true), {
        "type": "boolean",
        "enum": [true]
      })
    })
  })

  describe("Union", () => {
    it("oneOf", () => {
      assertJsonSchema(Schema.Union([Schema.String, Schema.Finite], { mode: "oneOf" }), {
        "anyOf": [
          { "type": "string" },
          { "type": "number" }
        ]
      })
    })

    it("Union(NonEmptyString, Int)", () => {
      assertJsonSchema(Schema.Union([Schema.NonEmptyString, Schema.Int]), {
        "anyOf": [
          { "type": "string", "description": "a value with a length of at least 1" },
          { "type": "integer" }
        ]
      })
    })
  })

  describe("Array", () => {
    it("Array(String)", () => {
      assertJsonSchema(Schema.Array(Schema.String), {
        "type": "array",
        "items": { "type": "string" }
      })
      assertJsonSchema(Schema.Array(Schema.String).annotate({ description: "description" }), {
        "type": "array",
        "items": { "type": "string" },
        "description": "description"
      })
    })

    it("Array(String) + isMinLength", () => {
      assertJsonSchema(Schema.Array(Schema.String).check(Schema.isMinLength(1)), {
        "type": "array",
        "items": { "type": "string" },
        "description": "a value with a length of at least 1",
        "minItems": 1
      })
    })

    it("Array(String) + isMinLength + isMaxLength", () => {
      assertJsonSchema(Schema.Array(Schema.String).check(Schema.isMinLength(1), Schema.isMaxLength(2)), {
        "type": "array",
        "items": { "type": "string" },
        "description": "a value with a length of at least 1 and a value with a length of at most 2",
        "minItems": 1,
        "maxItems": 2
      })
    })
  })

  describe("Tuple", () => {
    it("Tuple([])", () => {
      assertJsonSchema(Schema.Tuple([]), {
        "type": "array",
        "items": false
      })
    })

    it("Tuple([String, Finite])", async () => {
      const schema = Schema.Tuple([Schema.String, Schema.Finite])
      assertJsonSchema(schema, {
        "type": "object",
        "description":
          "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements",
        "properties": {
          "0": { "type": "string" },
          "1": { "type": "number" }
        },
        "required": ["0", "1"],
        "additionalProperties": false
      })

      const codec = toCodecOpenAI(schema).codec
      const asserts = new TestSchema.Asserts(codec)

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

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

    it("Tuple([String, Finite]) + description", () => {
      const schema = Schema.Tuple([Schema.String, Schema.Finite]).annotate({ description: "description" })
      assertJsonSchema(schema, {
        "type": "object",
        "description":
          "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements; description",
        "properties": {
          "0": { "type": "string" },
          "1": { "type": "number" }
        },
        "required": ["0", "1"],
        "additionalProperties": false
      })
    })

    it("Tuple([String, Finite?])", async () => {
      const schema = Schema.Tuple([Schema.String, Schema.optionalKey(Schema.Finite)])
      assertJsonSchema(schema, {
        "type": "object",
        "description":
          "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements",
        "properties": {
          "0": { "type": "string" },
          "1": {
            "anyOf": [
              { "type": "number" },
              { "type": "null" }
            ]
          }
        },
        "required": ["0", "1"],
        "additionalProperties": false
      })

      const codec = toCodecOpenAI(schema).codec
      const asserts = new TestSchema.Asserts(codec)

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

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

  describe("TupleWithRest", () => {
    it("TupleWithRest([String, Finite], Boolean)", async () => {
      const schema = Schema.TupleWithRest(
        Schema.Tuple([Schema.String, Schema.Finite]),
        [Schema.Boolean]
      )
      assertJsonSchema(schema, {
        "type": "object",
        "description":
          "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements",
        "properties": {
          "0": { "type": "string" },
          "1": { "type": "number" },
          "__rest__": { "type": "array", "items": { "type": "boolean" } }
        },
        "required": ["0", "1", "__rest__"],
        "additionalProperties": false
      })
      const codec = toCodecOpenAI(schema).codec
      const asserts = new TestSchema.Asserts(codec)

      const encoding = asserts.encoding()
      await encoding.succeed(["a", 1], { "0": "a", "1": 1, "__rest__": [] })
      await encoding.succeed(["a", 1, true], { "0": "a", "1": 1, "__rest__": [true] })
      await encoding.succeed(["a", 1, true, false], { "0": "a", "1": 1, "__rest__": [true, false] })

      const decoding = asserts.decoding()
      await decoding.succeed({ "0": "a", "1": 1, "__rest__": [] }, ["a", 1])
      await decoding.succeed({ "0": "a", "1": 1, "__rest__": [true] }, ["a", 1, true])
      await decoding.succeed({ "0": "a", "1": 1, "__rest__": [true, false] }, ["a", 1, true, false])
    })
  })

  describe("Struct", () => {
    it("required properties", () => {
      assertJsonSchema(Schema.Struct({ a: Schema.String }), {
        "type": "object",
        "properties": {
          "a": { "type": "string" }
        },
        "required": ["a"],
        "additionalProperties": false
      })
    })

    it("optional properties", async () => {
      const schema = Schema.Struct({
        a: Schema.String,
        b: Schema.optionalKey(Schema.Finite)
      })
      assertJsonSchema(
        schema,
        {
          "type": "object",
          "properties": {
            "a": { "type": "string" },
            "b": {
              "anyOf": [
                { "type": "number" },
                { "type": "null" }
              ]
            }
          },
          "required": ["a", "b"],
          "additionalProperties": false
        }
      )
      const codec = toCodecOpenAI(schema).codec
      const asserts = new TestSchema.Asserts(codec)

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

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

  it("Class", () => {
    class Person extends Schema.Class<Person>("Person")({
      name: Schema.String
    }) {}

    assertJsonSchema(Person, {
      "type": "object",
      "properties": {
        "name": { "type": "string" }
      },
      "required": ["name"],
      "additionalProperties": false,
      "$defs": {
        "Person": {
          "type": "object",
          "properties": {
            "name": { "type": "string" }
          },
          "required": ["name"],
          "additionalProperties": false
        }
      }
    })
  })

  describe("Record", () => {
    it("EmptyParams", () => {
      assertJsonSchema(Tool.EmptyParams, {
        "type": "object",
        "properties": {},
        "additionalProperties": false
      })
      assertJsonSchema(Schema.Record(Schema.String, Schema.Never), {
        "type": "object",
        "properties": {},
        "additionalProperties": false
      })
    })

    it("Record(String, Finite)", async () => {
      const schema = Schema.Record(Schema.String, Schema.Finite)
      assertJsonSchema(schema, {
        "type": "array",
        "description": "Object encoded as array of [key, value] pairs. Apply object constraints to the decoded object",
        "items": {
          "type": "object",
          "description":
            "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements",
          "properties": {
            "0": { "type": "string" },
            "1": { "type": "number" }
          },
          "required": ["0", "1"],
          "additionalProperties": false
        }
      })
      const codec = toCodecOpenAI(schema).codec
      const asserts = new TestSchema.Asserts(codec)

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

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

    it("Record(String, Finite) + description", () => {
      const schema = Schema.Record(Schema.String, Schema.Finite).annotate({ description: "description" })
      assertJsonSchema(schema, {
        "type": "array",
        "description":
          "Object encoded as array of [key, value] pairs. Apply object constraints to the decoded object; description",
        "items": {
          "type": "object",
          "description":
            "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements",
          "properties": {
            "0": { "type": "string" },
            "1": { "type": "number" }
          },
          "required": ["0", "1"],
          "additionalProperties": false
        }
      })
    })

    it("Record(String, Finite) + isMinProperties", () => {
      const schema = Schema.Record(Schema.String, Schema.Finite).check(Schema.isMinProperties(2))
      assertJsonSchema(schema, {
        "type": "array",
        "description":
          "Object encoded as array of [key, value] pairs. Apply object constraints to the decoded object; a value with at least 2 entries",
        "items": {
          "type": "object",
          "description":
            "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements",
          "properties": {
            "0": { "type": "string" },
            "1": { "type": "number" }
          },
          "required": ["0", "1"],
          "additionalProperties": false
        }
      })
    })

    it("Record(String, Finite) + isMinProperties + description", () => {
      const schema = Schema.Record(Schema.String, Schema.Finite).check(
        Schema.isMinProperties(2, { description: "description" })
      )
      assertJsonSchema(schema, {
        "type": "array",
        "description":
          "Object encoded as array of [key, value] pairs. Apply object constraints to the decoded object; description",
        "items": {
          "type": "object",
          "description":
            "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements",
          "properties": {
            "0": { "type": "string" },
            "1": { "type": "number" }
          },
          "required": ["0", "1"],
          "additionalProperties": false
        }
      })
    })
  })
})
