import type { z } from "zod";
import {
  collectSchemaFields,
  readEnumValues,
  readIntegerConstraint,
  readNumberConstraint,
  readSchemaFormat,
  readSchemaPattern,
  readSchemaType,
  toJsonSchema,
} from "./schema-introspection.js";
import type { BoundaryCase, BoundaryPartition } from "./types.js";

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");
  }
  if (typeof (schema as { toJSONSchema?: unknown }).toJSONSchema !== "function") {
    throw new Error("schema must expose toJSONSchema()");
  }
}

function pushCase(
  cases: BoundaryCase[],
  path: string,
  partition: BoundaryPartition,
  value: unknown,
  expectValid: boolean,
  description: string,
): void {
  cases.push({
    caseId: `${path}:${partition}`,
    path,
    partition,
    value,
    expectValid,
    description,
  });
}

function compareUnicodeScalars(left: string, right: string): number {
  let leftIndex = 0;
  let rightIndex = 0;
  while (leftIndex < left.length && rightIndex < right.length) {
    const leftCode = left.codePointAt(leftIndex);
    const rightCode = right.codePointAt(rightIndex);
    if (leftCode === undefined || rightCode === undefined) {
      break;
    }
    if (leftCode !== rightCode) {
      return leftCode < rightCode ? -1 : 1;
    }
    leftIndex += leftCode > 0xffff ? 2 : 1;
    rightIndex += rightCode > 0xffff ? 2 : 1;
  }
  return left.length - right.length;
}

function invalidEnumValue(values: string[]): string {
  const candidates = ["__invalid__", "unknown", "not-a-member"];
  for (const candidate of candidates) {
    if (!values.includes(candidate)) {
      return candidate;
    }
  }
  return `${values[0] ?? "valid"}-invalid`;
}

function invalidUuid(): string {
  return "not-a-uuid";
}

function generateFieldBoundaryCases(field: ReturnType<typeof collectSchemaFields>[number]): BoundaryCase[] {
  const cases: BoundaryCase[] = [];
  const { path, jsonSchema, optional, nullable } = field;
  const schemaType = readSchemaType(jsonSchema);

  if (optional) {
    pushCase(cases, path, "missing", undefined, true, "optional field omitted");
  }
  if (nullable || optional) {
    pushCase(cases, path, "null", null, nullable, "nullable field receives null");
  }

  if (schemaType === "string") {
    pushCase(cases, path, "empty", "", false, "empty string at string boundary");
    const minLength = readIntegerConstraint(jsonSchema, "minLength");
    const maxLength = readIntegerConstraint(jsonSchema, "maxLength");
    if (minLength !== undefined) {
      pushCase(cases, path, "min", "a".repeat(minLength), true, "minimum string length");
      if (minLength > 0) {
        pushCase(
          cases,
          path,
          "just-outside",
          "a".repeat(Math.max(0, minLength - 1)),
          false,
          "string shorter than minimum",
        );
      }
    }
    if (maxLength !== undefined) {
      pushCase(cases, path, "max", "a".repeat(maxLength), true, "maximum string length");
      pushCase(
        cases,
        path,
        "just-outside",
        "a".repeat(maxLength + 1),
        false,
        "string longer than maximum",
      );
    }

    const format = readSchemaFormat(jsonSchema);
    if (format === "uuid") {
      pushCase(
        cases,
        path,
        "encoding",
        invalidUuid(),
        false,
        "malformed uuid encoding",
      );
    } else if (format !== undefined) {
      pushCase(
        cases,
        path,
        "encoding",
        `not-${format}`,
        false,
        `malformed ${format} encoding`,
      );
    }

    const pattern = readSchemaPattern(jsonSchema);
    if (pattern !== undefined) {
      pushCase(cases, path, "encoding", "INVALID_PATTERN", false, "pattern encoding violation");
    }

    pushCase(cases, path, "malformed", 123, false, "non-string type at string boundary");
  }

  if (schemaType === "number" || schemaType === "integer") {
    const minimum = readNumberConstraint(jsonSchema, "minimum");
    const maximum = readNumberConstraint(jsonSchema, "maximum");
    const multipleOf = readNumberConstraint(jsonSchema, "multipleOf");

    if (minimum !== undefined) {
      pushCase(cases, path, "min", minimum, true, "minimum numeric value");
      pushCase(cases, path, "just-outside", minimum - 1, false, "below minimum numeric value");
      pushCase(cases, path, "just-inside", minimum, true, "at minimum numeric value");
    }
    if (maximum !== undefined) {
      pushCase(cases, path, "max", maximum, true, "maximum numeric value");
      pushCase(cases, path, "just-outside", maximum + 1, false, "above maximum numeric value");
      pushCase(cases, path, "just-inside", maximum, true, "at maximum numeric value");
    }
    if (multipleOf !== undefined) {
      const valid = multipleOf * 3;
      pushCase(cases, path, "precision", valid, true, "value aligned to multipleOf precision");
      pushCase(
        cases,
        path,
        "precision",
        valid + multipleOf / 10,
        false,
        "value violating multipleOf precision",
      );
    }

    pushCase(cases, path, "malformed", "not-a-number", false, "non-number type at numeric boundary");
  }

  const enumValues = readEnumValues(jsonSchema);
  if (enumValues !== undefined && enumValues.length > 0) {
    pushCase(cases, path, "enum", enumValues[0], true, "valid enum member");
    pushCase(
      cases,
      path,
      "enum",
      invalidEnumValue(enumValues),
      false,
      "invalid enum member",
    );
  }

  if (schemaType === "array") {
    const minItems = readIntegerConstraint(jsonSchema, "minItems");
    const maxItems = readIntegerConstraint(jsonSchema, "maxItems");
    if (minItems !== undefined) {
      pushCase(
        cases,
        path,
        "cardinality",
        Array.from({ length: minItems }, (_, index) => `item-${String(index)}`),
        true,
        "minimum array cardinality",
      );
      if (minItems > 0) {
        pushCase(
          cases,
          path,
          "cardinality",
          Array.from({ length: minItems - 1 }, (_, index) => `item-${String(index)}`),
          false,
          "below minimum array cardinality",
        );
      }
    }
    if (maxItems !== undefined) {
      pushCase(
        cases,
        path,
        "cardinality",
        Array.from({ length: maxItems }, (_, index) => `item-${String(index)}`),
        true,
        "maximum array cardinality",
      );
      pushCase(
        cases,
        path,
        "cardinality",
        Array.from({ length: maxItems + 1 }, (_, index) => `item-${String(index)}`),
        false,
        "above maximum array cardinality",
      );
    }
    pushCase(cases, path, "empty", [], minItems === undefined || minItems === 0, "empty array");
    if (field.hasUniquenessRefine) {
      pushCase(
        cases,
        path,
        "uniqueness",
        ["alpha", "alpha"],
        false,
        "duplicate array entries violate uniqueness",
      );
    }
    pushCase(cases, path, "malformed", "not-an-array", false, "non-array type at array boundary");
  }

  if (field.relationOwnership) {
    pushCase(
      cases,
      path,
      "relation-ownership",
      "tenant-b",
      false,
      "cross-tenant ownership identifier",
    );
  }

  return cases;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}

export function generateBoundaryCases(schema: z.ZodType): BoundaryCase[] {
  assertZodSchema(schema);
  const fields = collectSchemaFields(schema);
  const cases = fields.flatMap((field) => generateFieldBoundaryCases(field));

  const rootJsonSchema = toJsonSchema(schema);
  if (readSchemaType(rootJsonSchema) === "object") {
    const required = rootJsonSchema.required;
    if (Array.isArray(required)) {
      for (const key of required) {
        if (typeof key === "string") {
          pushCase(cases, key, "missing", undefined, false, "required object field omitted");
        }
      }
    }

    const properties = rootJsonSchema.properties;
    if (isRecord(properties)) {
      const keys = Object.keys(properties);
      const hasTenant = keys.some((key) => /tenantId$/i.test(key));
      const hasOwner = keys.some((key) => /(ownerId|resourceOwnerId)$/i.test(key));
      if (hasTenant && hasOwner) {
        pushCase(
          cases,
          "$",
          "relation-ownership",
          { tenantId: "tenant-a", resourceOwnerId: "tenant-b" },
          false,
          "resource owner does not match tenant boundary",
        );
      }
    }
  }

  return cases.sort((left, right) => {
    const pathCompare = compareUnicodeScalars(left.path, right.path);
    if (pathCompare !== 0) {
      return pathCompare;
    }
    return compareUnicodeScalars(left.partition, right.partition);
  });
}
