package com.jacoby6000.smithplates.sql.service.query.renderer.postgres

import com.jacoby6000.smithplates.sql.*
import com.jacoby6000.smithplates.sql.service.SqlModelExtractor
import com.jacoby6000.smithplates.sql.service.SqlQueries
import com.jacoby6000.smithplates.sql.service.query.renderer.SqlQueryRenderer
import software.amazon.smithy.model.shapes.ShapeId

final class PostgresSqlSelectRendererSpec extends munit.FunSuite {

  test("StarProjection - renders default as explicit table columns") {
    val statement =
      PostgresSqlSelectRendererSpec.internal.renderer
        .renderQueryUnits(PostgresSqlSelectRendererSpec.internal.starSchema.queries)
        .find(_.shapeId == ShapeId.from("example#ListItems"))
        .map { query =>
          SqlBindPlaceholder.format(
            query.statement.segments,
            PostgresSqlSelectRendererSpec.internal.renderer.migrationBindPlaceholder
          )
        }
        .getOrElse(fail("query 'example#ListItems' was not rendered"))

    assertEquals(
      statement,
      """SELECT i.id AS i_id, i.category_id AS i_category_id, i.name AS i_name, c.id AS c_id, c.name AS c_name
        |FROM items AS i
        |INNER JOIN categories AS c ON i.category_id = c.id
        |WHERE i.category_id = $1;""".stripMargin
    )
  }

  test("DeriveSelect - renders join, filters, and HAVING placeholders") {
    assertEquals(
      PostgresSqlSelectRendererSpec.internal.queryStatement(
        PostgresSqlSelectRendererSpec.internal.schema.queries,
        PostgresSqlSelectRendererSpec.internal.renderer),
      """SELECT i.id AS itemId, c.name AS categoryName, COUNT(i.id) AS itemCount
        |FROM items AS i
        |INNER JOIN categories AS c ON i.category_id = c.id
        |WHERE i.category_id = $1
        |GROUP BY i.id, c.name
        |HAVING COUNT(i.id) = $2;""".stripMargin
    )
  }
}
object PostgresSqlSelectRendererSpec {

  /** Internal implementation surface — not part of the stable API; subject to change without notice. */
  object internal {
    lazy val renderer                                                           =
      PostgresSqlQueryRenderer(
        migrationBindPlaceholder = SqlBindPlaceholder("$" + SqlBindPlaceholder.NumberToken),
        codegenBindPlaceholder = SqlBindPlaceholder("%s")
      )
    def queryStatement(queries: SqlQueries, renderer: SqlQueryRenderer): String =
      renderer
        .renderQueryUnits(queries)
        .find(_.shapeId == ShapeId.from("example#ListItemCategories"))
        .map { query =>
          SqlBindPlaceholder.format(
            query.statement.segments,
            renderer.migrationBindPlaceholder
          )
        }
        .getOrElse(throw new AssertionError("query 'example#ListItemCategories' was not rendered"))
    lazy val starQueryModel                                                     = SqlTestModelBuilder.assemble("""
      |use smithplates.codegen.sql#DerivedStruct
      |use smithplates.codegen.sql#sqlDeriveSelect
      |use smithplates.codegen.sql#sqlForeignKey
      |use smithplates.codegen.sql#sqlPrimaryKey
      |use smithplates.codegen.sql#sqlTable
      |use smithplates.codegen.sql#sqlVarchar
      |
      |@sqlTable(name: "categories")
      |structure Category {
      |    @sqlPrimaryKey
      |    id: String
      |    name: String
      |}
      |
      |@sqlTable(name: "items")
      |structure Item {
      |    @sqlPrimaryKey
      |    id: String
      |    @sqlForeignKey(references: "example#Category")
      |    category_id: String
      |    @sqlVarchar(maxLength: 64)
      |    name: String
      |}
      |
      |structure ListItemsInput {
      |    category_id: String
      |}
      |
      |@sqlDeriveSelect(
      |    from: { table: "example#Item", alias: "i" },
      |    joins: [{ table: "example#Category", type: "inner", tableAlias: "c" }],
      |    where: [{ left: "i.category_id", operator: "=", right: "input.category_id" }]
      |)
      |operation ListItems {
      |    input: ListItemsInput
      |    output: DerivedStruct
      |}
      |""".stripMargin)
    lazy val starSchema                                                         = SqlModelExtractor.extractOrThrow(starQueryModel)
    lazy val queryModel                                                         = SqlTestModelBuilder.assemble("""
      |use smithplates.codegen.sql#DerivedStruct
      |use smithplates.codegen.sql#sqlDeriveSelect
      |use smithplates.codegen.sql#sqlForeignKey
      |use smithplates.codegen.sql#sqlPrimaryKey
      |use smithplates.codegen.sql#sqlTable
      |use smithplates.codegen.sql#sqlVarchar
      |
      |@sqlTable(name: "categories")
      |structure Category {
      |    @sqlPrimaryKey
      |    id: String
      |    name: String
      |}
      |
      |@sqlTable(name: "items")
      |structure Item {
      |    @sqlPrimaryKey
      |    id: String
      |    @sqlForeignKey(references: "example#Category")
      |    category_id: String
      |    @sqlVarchar(maxLength: 64)
      |    name: String
      |}
      |
      |structure ListItemCategoriesInput {
      |    category_id: String
      |    minCount: Integer
      |}
      |
      |@sqlDeriveSelect(
      |    from: { table: "example#Item", alias: "i" },
      |    joins: [{ table: "example#Category", type: "inner", tableAlias: "c" }],
      |    projections: [
      |        { alias: "itemId", source: "i.id" },
      |        { alias: "categoryName", source: "c.name" },
      |        { alias: "itemCount", aggregate: "count", source: "i.id" }
      |    ],
      |    where: [{ left: "i.category_id", operator: "=", right: "input.category_id" }],
      |    groupBy: ["i.id", "c.name"],
      |    having: [{ left: "itemCount", operator: "=", right: "input.minCount" }]
      |)
      |operation ListItemCategories {
      |    input: ListItemCategoriesInput
      |    output: DerivedStruct
      |}
      |""".stripMargin)
    lazy val schema                                                             = SqlModelExtractor.extractOrThrow(queryModel)
  }
}
