import type { z } from "zod";
import type {
  DecoderConstraints,
  DecoderScenario,
} from "../../../../universal/fixtures/decoder/types.js";
import {
  collectSchemaFields,
  readNumberConstraint,
  schemaCoercesInput,
} from "./schema-introspection.js";
import type { StrictDecoderGenerationSpec } from "./types.js";

function assertNonEmptyString(value: string, field: string): void {
  if (value.trim().length === 0) {
    throw new Error(`${field} must be a non-empty string`);
  }
}

function assertZodSchema(schema: unknown): asserts schema is z.ZodType {
  if (typeof schema !== "object" || schema === null || typeof (schema as z.ZodType).safeParse !== "function") {
    throw new Error("schema must be a zod schema");
  }
}

function defaultIntegerConstraints(): DecoderConstraints {
  return {
    rejectEmptyCoercion: true,
    rejectNullCoercion: true,
    decimalIntegerOnly: true,
    safeIntegerOnly: true,
    rejectNonFinite: true,
  };
}

function hasCoercedNumberField(schema: z.ZodType): boolean {
  return collectSchemaFields(schema).some((field) => {
    const type = field.jsonSchema.type;
    return (type === "number" || type === "integer") && schemaCoercesInput(field.schema);
  });
}

function pushIntegerScenario(
  scenarios: DecoderScenario[],
  scenarioId: string,
  input: unknown,
  constraints: DecoderConstraints,
): void {
  scenarios.push({
    scenarioId,
    kind: "integer",
    input,
    constraints,
    observation: { accepted: false },
  });
}

export function generateStrictDecoderCases(spec: StrictDecoderGenerationSpec): DecoderScenario[] {
  assertNonEmptyString(spec.id, "id");
  assertZodSchema(spec.schema);

  const constraints = defaultIntegerConstraints();
  const scenarios: DecoderScenario[] = [];

  if (hasCoercedNumberField(spec.schema)) {
    pushIntegerScenario(scenarios, `${spec.id}:empty-string-to-zero`, "", constraints);
    pushIntegerScenario(scenarios, `${spec.id}:null-to-zero`, null, constraints);
    pushIntegerScenario(scenarios, `${spec.id}:hex-notation`, "0x10", constraints);
    pushIntegerScenario(scenarios, `${spec.id}:scientific-notation`, "1e3", constraints);
    pushIntegerScenario(
      scenarios,
      `${spec.id}:positive-unsafe-integer`,
      "9007199254740993",
      constraints,
    );
    pushIntegerScenario(
      scenarios,
      `${spec.id}:negative-unsafe-integer`,
      "-9007199254740993",
      constraints,
    );
    pushIntegerScenario(scenarios, `${spec.id}:non-finite-infinity`, "Infinity", constraints);
    pushIntegerScenario(scenarios, `${spec.id}:non-finite-nan`, "NaN", constraints);
  } else {
    const numericFields = collectSchemaFields(spec.schema).filter((field) => {
      const type = field.jsonSchema.type;
      return type === "number" || type === "integer";
    });
    for (const field of numericFields) {
      const minimum = readNumberConstraint(field.jsonSchema, "minimum");
      const maximum = readNumberConstraint(field.jsonSchema, "maximum");
      if (minimum !== undefined && minimum < 0) {
        pushIntegerScenario(
          scenarios,
          `${spec.id}:${field.path}:negative-overflow`,
          String(minimum - 1),
          constraints,
        );
      }
      if (maximum !== undefined) {
        pushIntegerScenario(
          scenarios,
          `${spec.id}:${field.path}:positive-unsafe-integer`,
          "9007199254740993",
          constraints,
        );
      }
      pushIntegerScenario(
        scenarios,
        `${spec.id}:${field.path}:non-finite-infinity`,
        "Infinity",
        constraints,
      );
    }
  }

  if (spec.includeFallback === true) {
    scenarios.push({
      scenarioId: `${spec.id}:invalid-primary-shadows-valid-secondary`,
      kind: "fallback",
      candidates: [
        { value: "not-a-number", valid: false },
        { value: 42, valid: true },
      ],
      selectedIndex: 0,
      constraints,
      observation: { accepted: false },
    });
  }

  return scenarios;
}
