content: {-# LANGUAGE OverloadedStrings #-}
  
  module SchemaParserTest (tests) where
  
  import qualified Convex.Schema.Parser as Schema
  import qualified Data.Map as Map
  import Test.HUnit
  import Text.Parsec (runParserT)
  
  runSchemaTest :: String -> String -> Schema.ParsedFile -> Test
  runSchemaTest testName input expected =
    testName ~: TestCase $ do
      result <- Schema.parseSchema input
      case result of
        Left err -> assertFailure ("Parser failed: " ++ show err)
        Right parsedFile -> parsedFile @?= expected
  
  -- Test Case 1: A standard schema with mixed statement endings.
  sampleSchemaStandard :: String
  sampleSchemaStandard =
    unlines
      [ "import { defineSchema, defineTable, v } from \"convex/server\";",
        "",
        "export const roleEnum = v.union(v.literal(\"admin\"), v.literal(\"user\"));",
        "",
        "export const productEnum = v.union(v.literal(\"palaba\"), v.literal(\"audiorake\"))",
        "",
        "export default defineSchema({",
        "  users: defineTable({",
        "    name: v.string(),",
        "    role: roleEnum",
        "  }),",
        "});"
      ]
  
  expectedSchemaStandard :: Schema.ParsedFile
  expectedSchemaStandard =
    Schema.ParsedFile
      { Schema.parsedConstants =
          Map.fromList
            [ ("roleEnum", Schema.VUnion [Schema.VLiteral "admin", Schema.VLiteral "user"]),
              ("productEnum", Schema.VUnion [Schema.VLiteral "palaba", Schema.VLiteral "audiorake"])
            ],
        Schema.parsedSchema =
          Schema.Schema
            { Schema.getTables =
                [ Schema.Table
                    { Schema.tableName = "users",
                      Schema.tableFields =
                        [ Schema.Field "name" Schema.VString,
                          Schema.Field "role" (Schema.VReference "roleEnum")
                        ],
                      Schema.tableIndexes = []
                    }
                ]
            }
      }
  
  -- Test Case 2: Schema definition appears at the top of the file.
  sampleSchemaAtTop :: String
  sampleSchemaAtTop =
    unlines
      [ "import { defineSchema, defineTable, v } from \"convex/server\";",
        "",
        "export default defineSchema({",
        "  projects: defineTable({",
        "    name: v.string(),",
        "    product: productEnum, // Reference to a const defined later",
        "  }),",
        "});",
        "",
        "// This constant is defined after it is referenced.",
        "export const productEnum = v.union(v.literal(\"a\"), v.literal(\"b\"));"
      ]
  
  expectedSchemaAtTop :: Schema.ParsedFile
  expectedSchemaAtTop =
    Schema.ParsedFile
      { Schema.parsedConstants =
          Map.fromList
            [("productEnum", Schema.VUnion [Schema.VLiteral "a", Schema.VLiteral "b"])],
        Schema.parsedSchema =
          Schema.Schema
            { Schema.getTables =
                [ Schema.Table
                    { Schema.tableName = "projects",
                      Schema.tableFields =
                        [ Schema.Field "name" Schema.VString,
                          Schema.Field "product" (Schema.VReference "productEnum")
                        ],
                      Schema.tableIndexes = []
                    }
                ]
            }
      }
  
  -- Test Case 3: A table uses a reference to a const object for its definition.
  sampleSchemaWithObjectRef :: String
  sampleSchemaWithObjectRef =
    unlines
      [ "import { v, defineSchema, defineTable } from \"convex/server\";",
        "",
        "// A top-level constant defining the fields for a table.",
        "const authCodeValidator = {",
        "  exchange_code: v.string(),",
        "  tenant_id: v.id(\"tenants\"),",
        "};",
        "// A type alias, which should be parsed and stored.",
        "export type MyId = typeof v.id(\"tenants\");",
        "",
        "export default defineSchema({",
        "  // This table uses the constant directly.",
        "  auth_code_sessions: defineTable(authCodeValidator),",
        "});"
      ]
  
  expectedSchemaWithObjectRef :: Schema.ParsedFile
  expectedSchemaWithObjectRef =
    Schema.ParsedFile
      { Schema.parsedConstants =
          Map.fromList
            [ ( "authCodeValidator",
                Schema.VObject
                  [ ("exchange_code", Schema.VString),
                    ("tenant_id", Schema.VId "tenants")
                  ]
              ),
              ("MyId", Schema.VId "tenants")
            ],
        Schema.parsedSchema =
          Schema.Schema
            { Schema.getTables =
                [ Schema.Table
                    { Schema.tableName = "auth_code_sessions",
                      -- The parser should expand the reference into the actual fields.
                      Schema.tableFields =
                        [ Schema.Field "exchange_code" Schema.VString,
                          Schema.Field "tenant_id" (Schema.VId "tenants")
                        ],
                      Schema.tableIndexes = []
                    }
                ]
            }
      }
  
  sampleComplexNoSemicolons :: String
  sampleComplexNoSemicolons =
    unlines
      [ "import { v, defineSchema, defineTable } from \"convex/server\"",
        "",
        "export const statusEnum = v.union(v.literal(\"pending\"), v.literal(\"complete\"))",
        "",
        "export default defineSchema({",
        "  tasks: defineTable({",
        "    title: v.string(),",
        "    status: statusEnum,",
        "  }).index(\"by_status\", [\"status\"])",
        "   .index(\"by_title\", [\"title\"]),",
        "",
        "  // This table uses the constant directly.",
        "  sessions: defineTable(sessionValidator)",
        "})",
        "",
        "// This constant is defined after it is referenced.",
        "const sessionValidator = {",
        "  userId: v.id(\"users\"),",
        "  token: v.string()",
        "}",
        "",
        "export type ComplexType = typeof sessionValidator"
      ]
  
  expectedComplexNoSemicolons :: Schema.ParsedFile
  expectedComplexNoSemicolons =
    Schema.ParsedFile
      { Schema.parsedConstants =
          Map.fromList
            [ ("statusEnum", Schema.VUnion [Schema.VLiteral "pending", Schema.VLiteral "complete"]),
              ( "sessionValidator",
                Schema.VObject
                  [ ("userId", Schema.VId "users"),
                    ("token", Schema.VString)
                  ]
              ),
              ("ComplexType", Schema.VReference "sessionValidator")
            ],
        Schema.parsedSchema =
          Schema.Schema
            { Schema.getTables =
                [ Schema.Table
                    { Schema.tableName = "tasks",
                      Schema.tableFields =
                        [ Schema.Field "title" Schema.VString,
                          Schema.Field "status" (Schema.VReference "statusEnum")
                        ],
                      Schema.tableIndexes = [Schema.Index "by_status" ["status"], Schema.Index "by_title" ["title"]]
                    },
                  Schema.Table
                    { Schema.tableName = "sessions",
                      Schema.tableFields =
                        [ Schema.Field "userId" (Schema.VId "users"),
                          Schema.Field "token" Schema.VString
                        ],
                      Schema.tableIndexes = []
                    }
                ]
            }
      }
  
  sampleOptionalArrayOfObjects :: String
  sampleOptionalArrayOfObjects =
    unlines
      [ "import { v, defineSchema, defineTable } from \"convex/server\"",
        "export const accessLevelEnum = v.union(v.literal(\"read\"), v.literal(\"write\"))",
        "export default defineSchema({",
        "  projects: defineTable({",
        "    shared_users: v.optional(",
        "      v.array(",
        "        v.object({",
        "          user_id: v.id(\"users\"),",
        "          access: accessLevelEnum",
        "        })",
        "      )",
        "    )",
        "  })",
        "})"
      ]
  
  expectedOptionalArrayOfObjects :: Schema.ParsedFile
  expectedOptionalArrayOfObjects =
    Schema.ParsedFile
      { Schema.parsedConstants =
          Map.fromList
            [("accessLevelEnum", Schema.VUnion [Schema.VLiteral "read", Schema.VLiteral "write"])],
        Schema.parsedSchema =
          Schema.Schema
            { Schema.getTables =
                [ Schema.Table
                    { Schema.tableName = "projects",
                      Schema.tableFields =
                        [ Schema.Field
                            "shared_users"
                            ( Schema.VOptional
                                ( Schema.VArray
                                    ( Schema.VObject
                                        [ ("user_id", Schema.VId "users"),
                                          ("access", Schema.VReference "accessLevelEnum")
                                        ]
                                    )
                                )
                            )
                        ],
                      Schema.tableIndexes = []
                    }
                ]
            }
      }
  
  sampleArrayOfPrimitives :: String
  sampleArrayOfPrimitives =
    unlines
      [ "import { v, defineSchema, defineTable } from \"convex/server\"",
        "export default defineSchema({",
        "  analytics: defineTable({",
        "    summary: v.array(v.number()),",
        "    length: v.number(),",
        "  })",
        "})"
      ]
  
  expectedArrayOfPrimitives :: Schema.ParsedFile
  expectedArrayOfPrimitives =
    Schema.ParsedFile
      { Schema.parsedConstants = Map.empty,
        Schema.parsedSchema =
          Schema.Schema
            { Schema.getTables =
                [ Schema.Table
                    { Schema.tableName = "analytics",
                      Schema.tableFields =
                        [ Schema.Field "summary" (Schema.VArray Schema.VNumber),
                          Schema.Field "length" Schema.VNumber
                        ],
                      Schema.tableIndexes = []
                    }
                ]
            }
      }
  
  -- Test Case for sanitizing union literals
  sampleSchemaWithUnionsToSanitize :: String
  sampleSchemaWithUnionsToSanitize =
    unlines
      [ "import { defineSchema, defineTable, v } from \"convex/server\";",
        "",
        "export const instruction_mime_type = v.union(",
        "  v.literal(\"application/pdf\"),",
        "  v.literal(\"text/html\"),",
        "  v.literal(\"text/plain\"),",
        "  v.literal(\"user@example.com\")",
        ");",
        "",
        "export default defineSchema({",
        "  documents: defineTable({",
        "    name: v.string(),",
        "    mime_type: instruction_mime_type",
        "  })",
        "});"
      ]
  
  expectedSchemaWithSanitizedUnions :: Schema.ParsedFile
  expectedSchemaWithSanitizedUnions =
    Schema.ParsedFile
      { Schema.parsedConstants =
          Map.fromList
            [ ( "instruction_mime_type",
                Schema.VUnion
                  [ Schema.VLiteral "application/pdf",
                    Schema.VLiteral "text/html",
                    Schema.VLiteral "text/plain",
                    Schema.VLiteral "user@example.com"
                  ]
              )
            ],
        Schema.parsedSchema =
          Schema.Schema
            { Schema.getTables =
                [ Schema.Table
                    { Schema.tableName = "documents",
                      Schema.tableFields =
                        [ Schema.Field "name" Schema.VString,
                          Schema.Field "mime_type" (Schema.VReference "instruction_mime_type")
                        ],
                      Schema.tableIndexes = []
                    }
                ]
            }
      }
  
  sampleSchemaWithVectorIndex :: String
  sampleSchemaWithVectorIndex =
    unlines
      [ "import { v, defineSchema, defineTable } from \"convex/server\";",
        "",
        "export default defineSchema({",
        "  embeddings: defineTable({",
        "    creator: v.string(),",
        "    linked_block: v.id(\"parsing_cache\"),",
        "    embedding: v.array(v.float64()),",
        "  }).vectorIndex(\"by_embedding\", {",
        "    vectorField: \"embedding\",",
        "    dimensions: 1000,",
        "    filterFields: [\"creator\"],",
        "  }),",
        "});"
      ]
  
  expectedSchemaWithVectorIndex :: Schema.ParsedFile
  expectedSchemaWithVectorIndex =
    Schema.ParsedFile
      { Schema.parsedConstants = Map.empty,
        Schema.parsedSchema =
          Schema.Schema
            { Schema.getTables =
                [ Schema.Table
                    { Schema.tableName = "embeddings",
                      Schema.tableFields =
                        [ Schema.Field "creator" Schema.VString,
                          Schema.Field "linked_block" (Schema.VId "parsing_cache"),
                          Schema.Field "embedding" (Schema.VArray Schema.VFloat64)
                        ],
                      Schema.tableIndexes =
                        [ Schema.VectorIndex
                            { Schema.indexName = "by_embedding",
                              Schema.indexVectorField = "embedding",
                              Schema.indexDimensions = 1000,
                              Schema.indexFilterFields = ["creator"]
                            }
                        ]
                    }
                ]
            }
      }
  
  -- Main test suite combining all cases.
  tests :: Test
  tests =
    "Schema Parser"
      ~: test
        [ runSchemaTest "parses standard schema with mixed endings" sampleSchemaStandard expectedSchemaStandard,
          runSchemaTest "parses schema defined before its constants" sampleSchemaAtTop expectedSchemaAtTop,
          runSchemaTest "parses table defined with an object reference" sampleSchemaWithObjectRef expectedSchemaWithObjectRef,
          runSchemaTest "parses complex schema without semicolons" sampleComplexNoSemicolons expectedComplexNoSemicolons,
          runSchemaTest "parses optional array of objects" sampleOptionalArrayOfObjects expectedOptionalArrayOfObjects,
          runSchemaTest "parses array of primitives" sampleArrayOfPrimitives expectedArrayOfPrimitives,
          runSchemaTest "sanitizes union literals with special characters" sampleSchemaWithUnionsToSanitize expectedSchemaWithSanitizedUnions,
          runSchemaTest "parses vector indexes" sampleSchemaWithVectorIndex expectedSchemaWithVectorIndex
        ]
  