import type { BaseFieldConfig, Field, TActionContext } from "data/fields";
import type { ColumnDataType } from "kysely";
import { omit } from "lodash-es";
import type { TestRunner } from "core/test";

type ConstructableField = new (name: string, config?: Partial<BaseFieldConfig>) => Field;

type FieldTestConfig = {
   defaultValue: any;
   sampleValues?: any[];
   schemaType: ColumnDataType;
};

export function transformPersist(field: Field, value: any, context?: TActionContext) {
   return field.transformPersist(value, undefined as any, context as any);
}

export function fieldTestSuite(
   testRunner: TestRunner,
   fieldClass: ConstructableField,
   config: FieldTestConfig,
   _requiredConfig: any = {},
) {
   const { test, expect } = testRunner;
   const noConfigField = new fieldClass("no_config", _requiredConfig);
   const fillable = new fieldClass("fillable", { ..._requiredConfig, fillable: true });
   const required = new fieldClass("required", { ..._requiredConfig, required: true });
   const hidden = new fieldClass("hidden", { ..._requiredConfig, hidden: true });
   const dflt = new fieldClass("dflt", { ..._requiredConfig, default_value: config.defaultValue });
   const requiredAndDefault = new fieldClass("full", {
      ..._requiredConfig,
      fillable: true,
      required: true,
      default_value: config.defaultValue,
   });

   test("schema", () => {
      expect(noConfigField.name).toBe("no_config");

      const { type, name, nullable, dflt } = noConfigField.schema()!;
      expect({ type, name, nullable, dflt }).toEqual({
         type: config.schemaType as any,
         name: "no_config",
         nullable: true, // always true
         dflt: undefined, // never using default value
      });
   });

   test("hasDefault", async () => {
      expect(noConfigField.hasDefault()).toBe(false);
      expect(noConfigField.getDefault()).toBeUndefined();
      expect(dflt.hasDefault()).toBe(true);
      expect(dflt.getDefault()).toEqual(config.defaultValue);
   });

   test("isFillable", async () => {
      expect(noConfigField.isFillable()).toBe(true);
      expect(fillable.isFillable()).toBe(true);
      expect(hidden.isFillable()).toBe(true);
      expect(required.isFillable()).toBe(true);
   });

   test("isHidden", async () => {
      expect(noConfigField.isHidden()).toBe(false);
      expect(hidden.isHidden()).toBe(true);
      expect(fillable.isHidden()).toBe(false);
      expect(required.isHidden()).toBe(false);
   });

   test("isRequired", async () => {
      expect(noConfigField.isRequired()).toBe(false);
      expect(required.isRequired()).toBe(true);
      expect(hidden.isRequired()).toBe(false);
      expect(fillable.isRequired()).toBe(false);
   });

   test.if(Array.isArray(config.sampleValues))("getValue (RenderContext)", async () => {
      const isPrimitive = (v) => ["string", "number"].includes(typeof v);
      for (const value of config.sampleValues!) {
         // "form"
         expect(isPrimitive(noConfigField.getValue(value, "form"))).toBe(true);
         // "table"
         expect(isPrimitive(noConfigField.getValue(value, "table"))).toBe(true);
         // "read"
         // "submit"
      }
   });

   test("transformPersist", async () => {
      const persist = await transformPersist(noConfigField, config.defaultValue);
      expect(config.defaultValue).toEqual(noConfigField.transformRetrieve(config.defaultValue));
      expect(transformPersist(noConfigField, null)).resolves.toBeUndefined();
      expect(transformPersist(noConfigField, undefined)).resolves.toBeUndefined();
      expect(transformPersist(requiredAndDefault, null)).resolves.toBe(persist);
      expect(transformPersist(dflt, null)).resolves.toBe(persist);
   });

   test("toJSON", async () => {
      const _config = {
         ..._requiredConfig,
         required: false,
      };

      function fieldJson(field: Field) {
         const json = field.toJSON();
         return {
            ...json,
            config: omit(json.config, ["html"]),
         };
      }

      expect(fieldJson(noConfigField)).toEqual({
         type: noConfigField.type,
         config: _config,
      });

      expect(fieldJson(fillable)).toEqual({
         type: noConfigField.type,
         config: {
            ..._config,
            fillable: true,
         },
      });

      expect(fieldJson(required)).toEqual({
         type: required.type,
         config: {
            ..._config,
            required: true,
         },
      });

      expect(fieldJson(hidden)).toEqual({
         type: required.type,
         config: {
            ..._config,
            hidden: true,
         },
      });

      expect(fieldJson(dflt)).toEqual({
         type: dflt.type,
         config: {
            ..._config,
            default_value: config.defaultValue,
         },
      });

      expect(fieldJson(requiredAndDefault)).toEqual({
         type: requiredAndDefault.type,
         config: {
            ..._config,
            fillable: true,
            required: true,
            default_value: config.defaultValue,
         },
      });
   });
}
