// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors

import * as arrow from "../lancedb/arrow";
import { sanitizeField, sanitizeMap, sanitizeType } from "../lancedb/sanitize";

describe("sanitize", function () {
  describe("sanitizeType function", function () {
    it("should handle type objects", function () {
      const type = new arrow.Int32();
      const result = sanitizeType(type);

      expect(result.typeId).toBe(arrow.Type.Int);
      expect((result as arrow.Int).bitWidth).toBe(32);
      expect((result as arrow.Int).isSigned).toBe(true);

      const floatType = {
        typeId: 3, // Type.Float = 3
        precision: 2,
        toString: () => "Float",
        isFloat: true,
        isFixedWidth: true,
      };

      const floatResult = sanitizeType(floatType);
      expect(floatResult).toBeInstanceOf(arrow.DataType);
      expect(floatResult.typeId).toBe(arrow.Type.Float);

      const floatResult2 = sanitizeType({ ...floatType, typeId: () => 3 });
      expect(floatResult2).toBeInstanceOf(arrow.DataType);
      expect(floatResult2.typeId).toBe(arrow.Type.Float);
    });

    const allTypeNameTestCases = [
      ["null", new arrow.Null()],
      ["binary", new arrow.Binary()],
      ["utf8", new arrow.Utf8()],
      ["bool", new arrow.Bool()],
      ["int8", new arrow.Int8()],
      ["int16", new arrow.Int16()],
      ["int32", new arrow.Int32()],
      ["int64", new arrow.Int64()],
      ["uint8", new arrow.Uint8()],
      ["uint16", new arrow.Uint16()],
      ["uint32", new arrow.Uint32()],
      ["uint64", new arrow.Uint64()],
      ["float16", new arrow.Float16()],
      ["float32", new arrow.Float32()],
      ["float64", new arrow.Float64()],
      ["datemillisecond", new arrow.DateMillisecond()],
      ["dateday", new arrow.DateDay()],
      ["timenanosecond", new arrow.TimeNanosecond()],
      ["timemicrosecond", new arrow.TimeMicrosecond()],
      ["timemillisecond", new arrow.TimeMillisecond()],
      ["timesecond", new arrow.TimeSecond()],
      ["intervaldaytime", new arrow.IntervalDayTime()],
      ["intervalyearmonth", new arrow.IntervalYearMonth()],
      ["durationnanosecond", new arrow.DurationNanosecond()],
      ["durationmicrosecond", new arrow.DurationMicrosecond()],
      ["durationmillisecond", new arrow.DurationMillisecond()],
      ["durationsecond", new arrow.DurationSecond()],
    ] as const;

    it.each(allTypeNameTestCases)(
      'should map type name "%s" to %s',
      function (name, expected) {
        const result = sanitizeType(name);
        expect(result).toBeInstanceOf(expected.constructor);
      },
    );

    const caseVariationTestCases = [
      ["NULL", new arrow.Null()],
      ["Utf8", new arrow.Utf8()],
      ["FLOAT32", new arrow.Float32()],
      ["DaTedAy", new arrow.DateDay()],
    ] as const;

    it.each(caseVariationTestCases)(
      'should be case insensitive for type name "%s" mapped to %s',
      function (name, expected) {
        const result = sanitizeType(name);
        expect(result).toBeInstanceOf(expected.constructor);
      },
    );

    it("should throw error for unrecognized type name", function () {
      expect(() => sanitizeType("invalid_type")).toThrow(
        "Unrecognized type name in schema: invalid_type",
      );
    });
  });

  describe("sanitizeField function", function () {
    it("should handle field with string type name", function () {
      const field = sanitizeField({
        name: "string_field",
        type: "utf8",
        nullable: true,
        metadata: new Map([["key", "value"]]),
      });

      expect(field).toBeInstanceOf(arrow.Field);
      expect(field.name).toBe("string_field");
      expect(field.type).toBeInstanceOf(arrow.Utf8);
      expect(field.nullable).toBe(true);
      expect(field.metadata?.get("key")).toBe("value");
    });

    it("should handle field with type object", function () {
      const floatType = {
        typeId: 3, // Float
        precision: 32,
      };

      const field = sanitizeField({
        name: "float_field",
        type: floatType,
        nullable: false,
      });

      expect(field).toBeInstanceOf(arrow.Field);
      expect(field.name).toBe("float_field");
      expect(field.type).toBeInstanceOf(arrow.DataType);
      expect(field.type.typeId).toBe(arrow.Type.Float);
      expect((field.type as arrow.Float64).precision).toBe(32);
      expect(field.nullable).toBe(false);
    });

    it("should handle field with direct Type instance", function () {
      const field = sanitizeField({
        name: "bool_field",
        type: new arrow.Bool(),
        nullable: true,
      });

      expect(field).toBeInstanceOf(arrow.Field);
      expect(field.name).toBe("bool_field");
      expect(field.type).toBeInstanceOf(arrow.Bool);
      expect(field.nullable).toBe(true);
    });

    it("should throw error for invalid field object", function () {
      expect(() =>
        sanitizeField({
          type: "int32",
          nullable: true,
        }),
      ).toThrow(
        "The field passed in is missing a `type`/`name`/`nullable` property",
      );

      // Invalid type
      expect(() =>
        sanitizeField({
          name: "invalid",
          type: { invalid: true },
          nullable: true,
        }),
      ).toThrow("Expected a Type to have a typeId property");

      // Invalid nullable
      expect(() =>
        sanitizeField({
          name: "invalid_nullable",
          type: "int32",
          nullable: "not a boolean",
        }),
      ).toThrow("The field passed in had a non-boolean `nullable` property");
    });

    it("should report error for invalid type name", function () {
      expect(() =>
        sanitizeField({
          name: "invalid_field",
          type: "invalid_type",
          nullable: true,
        }),
      ).toThrow(
        "Unable to sanitize type for field: invalid_field due to error: Error: Unrecognized type name in schema: invalid_type",
      );
    });
  });

  describe("sanitizeMap function", function () {
    it.each([
      ["no children", []],
      ["two children", [{}, {}]],
    ])("should reject a Map type with %s", function (_, children) {
      expect(() => sanitizeMap({ children, keysSorted: false })).toThrow(
        "Expected a Map type to have exactly one child",
      );
    });
  });
});
