import { dereference } from "@scalar/openapi-parser";
import { formatTypeSpec } from "@typespec/compiler";
import { strictEqual } from "node:assert";
import { beforeAll, describe, it } from "vitest";
import { Context, createContext } from "../../src/cli/actions/convert/utils/context.js";
import { OpenAPI3Document, OpenAPI3Schema, OpenAPISchema3_1, Refable } from "../../src/types.js";

interface TestScenario {
  schema: Refable<OpenAPI3Schema | OpenAPISchema3_1>;
  expected: string;
}

function generateScenarioName(scenario: TestScenario): string {
  return `${JSON.stringify(scenario.schema)} => ${scenario.expected}`;
}

const testScenarios: TestScenario[] = [
  // boolean
  { schema: { type: "boolean" }, expected: "boolean" },
  { schema: { type: "boolean", default: false }, expected: "boolean" },
  { schema: { type: "boolean", nullable: true }, expected: "boolean | null" },
  // integers
  { schema: { type: "integer" }, expected: "integer" },
  { schema: { type: "integer", format: "int8" }, expected: "int8" },
  { schema: { type: "integer", format: "int16" }, expected: "int16" },
  { schema: { type: "integer", format: "int32" }, expected: "int32" },
  { schema: { type: "integer", format: "int64" }, expected: "int64" },
  { schema: { type: "integer", format: "uint8" }, expected: "uint8" },
  { schema: { type: "integer", format: "uint16" }, expected: "uint16" },
  { schema: { type: "integer", format: "uint32" }, expected: "uint32" },
  { schema: { type: "integer", format: "uint64" }, expected: "uint64" },
  { schema: { type: "integer", format: "double-int" }, expected: "safeint" },
  { schema: { type: "integer", enum: [1, 3, 5, 8, 13] }, expected: "1 | 3 | 5 | 8 | 13" },
  {
    schema: { type: "integer", default: 3, enum: [1, 3, 5, 8, 13] },
    expected: "1 | 3 | 5 | 8 | 13 = 3",
  },
  {
    schema: { type: "integer", default: 3, enum: [1, 3, 5, 8, 13], nullable: true },
    expected: "1 | 3 | 5 | 8 | 13 | null = 3",
  },
  // numerics
  { schema: { type: "number" }, expected: "numeric" },
  { schema: { type: "number", default: 0 }, expected: "numeric" },
  { schema: { type: "number", default: 123 }, expected: "numeric = 123" },
  { schema: { type: "number", default: 123, nullable: true }, expected: "numeric | null = 123" },
  { schema: { type: "number", format: "decimal" }, expected: "decimal" },
  { schema: { type: "number", format: "decimal128" }, expected: "decimal128" },
  { schema: { type: "number", format: "double" }, expected: "float64" },
  { schema: { type: "number", format: "float" }, expected: "float32" },
  { schema: { type: "number", enum: [3.14, 6.28, 42] }, expected: "3.14 | 6.28 | 42" },
  // strings
  { schema: { type: "string" }, expected: "string" },
  {
    schema: { type: "string", default: "foo" },
    expected: 'string = "foo"',
  },
  { schema: { type: "string", default: "" }, expected: "string" },
  {
    schema: { type: "string", default: null as unknown as string, nullable: true },
    expected: "string | null",
  },
  { schema: { type: "string", format: "binary" }, expected: "bytes" },
  { schema: { type: "string", format: "byte" }, expected: "bytes" },
  // OpenAPI 3.1+ contentEncoding: base64 should be bytes
  { schema: { type: "string", contentEncoding: "base64" }, expected: "bytes" },
  { schema: { type: "string", format: "date" }, expected: "plainDate" },
  { schema: { type: "string", format: "date-time" }, expected: "utcDateTime" },
  { schema: { type: "string", format: "duration" }, expected: "duration" },
  { schema: { type: "string", format: "time" }, expected: "plainTime" },
  { schema: { type: "string", format: "uri" }, expected: "url" },
  { schema: { type: "string", enum: ["foo", "bar"] }, expected: `"foo" | "bar"` },
  {
    schema: { type: "string", default: "foo", enum: ["foo", "bar"] },
    expected: `"foo" | "bar" = "foo"`,
  },
  // const schemas
  { schema: { const: "output_audio_buffer.started" }, expected: `"output_audio_buffer.started"` },
  { schema: { const: 42 }, expected: `42` },
  { schema: { const: true }, expected: `true` },
  { schema: { const: false }, expected: `false` },
  { schema: { const: null }, expected: `null` },
  {
    schema: { const: "output_audio_buffer.started", nullable: true },
    expected: `"output_audio_buffer.started" | null`,
  },
  // refs
  { schema: { $ref: "#/Path/To/Some/Model" }, expected: "Model" },
  { schema: { $ref: "#/Path/To/Some/Model.Prop" }, expected: "Model.Prop" },
  // arrays
  { schema: { type: "array", items: { type: "string" } }, expected: "string[]" },
  {
    schema: { type: "array", items: { type: "array", items: { type: "string" } } },
    expected: "string[][]",
  },
  {
    schema: { type: "array", items: { type: "string", enum: ["foo", "bar"] } },
    expected: `("foo" | "bar")[]`,
  },
  { schema: { type: "array", items: { $ref: "#/Path/To/Some/Model" } }, expected: "Model[]" },
  {
    schema: { type: "array", items: { anyOf: [{ type: "string" }, { $ref: "#/Path/To/Model" }] } },
    expected: "(string | Model)[]",
  },
  // objects
  {
    schema: { type: "object", properties: { foo: { type: "string" } } },
    expected: "{foo?: string}",
  },
  {
    schema: {
      type: "object",
      required: ["foo"],
      properties: { foo: { type: "string" }, bar: { type: "boolean" } },
    },
    expected: "{foo: string; bar?: boolean}",
  },
  {
    schema: { type: "object", additionalProperties: { type: "string" } },
    expected: "Record<string>",
  },
  {
    schema: {
      type: "object",
      additionalProperties: { type: "string" },
      properties: { bar: { type: "boolean" } },
    },
    expected: "{bar?: boolean; ...Record<string>}",
  },
  // Test for additionalProperties: true (should be Record<unknown>)
  {
    schema: { type: "object", additionalProperties: true },
    expected: "Record<unknown>",
  },
  // Test for additionalProperties: {} (should be Record<unknown>, same as above)
  {
    schema: { type: "object", additionalProperties: {} },
    expected: "Record<unknown>",
  },
  // Test for additionalProperties: true with properties
  {
    schema: {
      type: "object",
      additionalProperties: true,
      properties: { bar: { type: "boolean" } },
    },
    expected: "{bar?: boolean; ...Record<unknown>}",
  },
  {
    schema: {
      type: "object",
      required: ["foo"],
      properties: { foo: { type: "object", properties: { foo: { type: "string" } } } },
    },
    expected: "{foo: {foo?: string}}",
  },
  {
    schema: {
      type: "object",
      required: ["foo"],
      properties: { foo: { type: "string" }, bar: { $ref: "#/Path/To/Model" } },
    },
    expected: "{foo: string; bar?: Model}",
  },
  // anyOf/oneOf
  {
    schema: {
      anyOf: [
        { $ref: "#/Path/To/Model" },
        { type: "boolean" },
        { type: "string", enum: ["foo", "bar"] },
      ],
    },
    expected: `Model | boolean | "foo" | "bar"`,
  },
  {
    schema: {
      oneOf: [
        { $ref: "#/Path/To/Model" },
        { type: "boolean" },
        { type: "string", enum: ["foo", "bar"] },
      ],
    },
    expected: `Model | boolean | "foo" | "bar"`,
  },
  // allOf
  {
    schema: {
      allOf: [{ $ref: "#/Path/To/BaseModel" }, { $ref: "#/Path/To/MixinModel" }],
    },
    expected: "BaseModel & MixinModel",
  },
  {
    schema: {
      allOf: [
        { $ref: "#/Path/To/BaseModel" },
        { $ref: "#/Path/To/MixinModel" },
        {
          type: "object",
          properties: {
            foo: { type: "string" },
            bar: { type: "boolean" },
          },
        },
      ],
    },
    expected: "BaseModel & MixinModel & {foo?: string; bar?: boolean}",
  },
  {
    schema: {
      allOf: [
        { $ref: "#/Path/To/BaseModel" },
        {
          type: "object",
          required: ["foo"],
          properties: {
            foo: { type: "string" },
            bar: { type: "boolean" },
          },
        },
      ],
    },
    expected: "BaseModel & {foo: string; bar?: boolean}",
  },
  {
    schema: {
      allOf: [
        {
          type: "object",
          required: ["prop1"],
          properties: {
            prop1: { type: "string" },
          },
        },
        {
          type: "object",
          properties: {
            prop2: {
              allOf: [
                { $ref: "#/Path/To/BaseModel" },
                { $ref: "#/Path/To/MixinModel" },
                {
                  type: "object",
                  required: ["foo"],
                  properties: {
                    foo: { type: "string" },
                    bar: { type: "boolean" },
                  },
                },
              ],
            },
          },
        },
      ],
    },
    expected: "{prop1: string; prop2?: BaseModel & MixinModel & {foo: string; bar?: boolean}}",
  },
  {
    schema: {
      type: "object",
      properties: {
        emptyProp: { type: "object" },
      },
    },
    expected: "{emptyProp?: {}}",
  },
  // fallthrough
  { schema: {}, expected: "unknown" },
  {
    schema: {
      type: "object",
      required: ["missingTypeProp"],
      properties: {
        missingTypeProp: { properties: { foo: { type: "string" } } },
      },
    },
    expected: "{missingTypeProp: { foo?: string}}",
  },
  // OpenAPI 3.1 type arrays
  { schema: { type: ["integer", "null"] as any, format: "int32" }, expected: "int32 | null" },
  { schema: { type: ["string", "null"] as any }, expected: "string | null" },
  { schema: { type: ["boolean", "null"] as any }, expected: "boolean | null" },
  { schema: { type: ["number", "null"] as any, format: "float" }, expected: "float32 | null" },
  {
    schema: { type: ["integer", "null"] as any, format: "int32", minimum: 1, maximum: 20 },
    expected: "int32 | null",
  },
  {
    schema: { type: ["string", "null"] as any, format: "date-time" },
    expected: "utcDateTime | null",
  },
  // Multiple non-null types in array (edge case - not supported, falls back to unknown)
  {
    schema: { type: ["string", "integer"] as any },
    expected: "unknown",
  },
  // Type array with three types including null (edge case - not supported, falls back to unknown)
  {
    schema: { type: ["string", "integer", "null"] as any },
    expected: "unknown",
  },
];

describe("tsp-openapi: generate-type", () => {
  let context: Context;
  beforeAll(async () => {
    const { specification } = await dereference({
      openapi: "3.0.0",
      info: { title: "Test", version: "1.0.0" },
      paths: {},
    });
    if (!specification) {
      throw new Error("Failed to dereference OpenAPI document");
    }
    context = createContext(specification as OpenAPI3Document);
  });
  testScenarios.forEach((t) =>
    it(`${generateScenarioName(t)}`, async () => {
      const type = context.generateTypeFromRefableSchema(t.schema, []);
      const wrappedType = await formatWrappedType(type);
      const wrappedExpected = await formatWrappedType(t.expected);
      strictEqual(wrappedType, wrappedExpected);
    }),
  );
});

// Wrap the expected and actual types in this model to get formatted types.
function formatWrappedType(type: string): Promise<string> {
  return formatTypeSpec(`model Test { test: ${type}; }`);
}
