import { expectAssignable, expectType } from 'tsd';

import type { PlainFieldContribution, JsonFieldValue } from './inputs';

//
// Parent fields (where `children` is set)
//

// Simplest `children: [{...}]` case.
const singleRequiredChild = {
  string_required: 'some_string',
};
type singleRequiredChildren = PlainFieldContribution<{
  key: 'parent_input';
  children: [{ key: 'string_required'; type: 'string'; required: true }];
}>;
expectAssignable<singleRequiredChildren>(singleRequiredChild);
expectAssignable<singleRequiredChildren>({
  string_required: 'some_string',
  parent_input: [{ string_required: 'value1' }, { string_required: 'value2' }],
});
expectType<singleRequiredChildren>(
  singleRequiredChild as {
    string_required: string;
    parent_input?: { string_required: string }[];
  },
);

// Complete `children: [{...}]` case.
const multipleRequiredChild = {
  string_required: 'some_string',
  number_required: 1,
  boolean_required: true,
  datetime_required: '2021-01-01T00:00:00Z',
  file_required: 'some_file',
  password_required: 'some_password',
  code_required: 'some_code',
};
const multipleRequiredChildWithLineItems = {
  string_required: 'some_string',
  number_required: 1,
  boolean_required: true,
  datetime_required: '2021-01-01T00:00:00Z',
  file_required: 'some_file',
  password_required: 'some_password',
  code_required: 'some_code',
  parent_input: [
    {
      string_required: 'item1',
      number_required: 2,
      boolean_required: false,
      datetime_required: '2022-01-01T00:00:00Z',
      file_required: 'file1',
      password_required: 'pass1',
      code_required: 'code1',
    },
  ],
};
type multipleRequiredChildren = PlainFieldContribution<{
  key: 'parent_input';
  children: [
    { key: 'string_required'; type: 'string'; required: true },
    { key: 'number_required'; type: 'number'; required: true },
    { key: 'boolean_required'; type: 'boolean'; required: true },
    { key: 'datetime_required'; type: 'datetime'; required: true },
    { key: 'file_required'; type: 'file'; required: true },
    { key: 'password_required'; type: 'password'; required: true },
    { key: 'code_required'; type: 'code'; required: true },
  ];
}>;
expectAssignable<multipleRequiredChildren>(multipleRequiredChild);
expectAssignable<multipleRequiredChildren>(multipleRequiredChildWithLineItems);
expectType<multipleRequiredChildren>(
  {} as {
    string_required: string;
    number_required: number;
    boolean_required: boolean;
    datetime_required: string;
    file_required: string;
    password_required: string;
    code_required: string;
    parent_input?: {
      string_required: string;
      number_required: number;
      boolean_required: boolean;
      datetime_required: string;
      file_required: string;
      password_required: string;
      code_required: string;
    }[];
  },
);

// Complete `children: [{...}]` case where all children are optional.
const multipleOptionalChildNoLineItems = {
  string_optional: 'some_string',
  number_optional: 1,
  boolean_optional: true,
  datetime_optional: '2021-01-01T00:00:00Z',
  file_optional: 'some_file',
  password_optional: 'some_password',
  code_optional: 'some_code',
};
const multipleOptionalChildWithLineItems = {
  string_optional: 'some_string',
  number_optional: 1,
  boolean_optional: true,
  datetime_optional: '2021-01-01T00:00:00Z',
  file_optional: 'some_file',
  password_optional: 'some_password',
  code_optional: 'some_code',
  parent_input: [{ string_optional: 'item1' }],
};
type multipleOptionalChildren = PlainFieldContribution<{
  key: 'parent_input';
  children: [
    { key: 'string_optional'; type: 'string'; required: false },
    { key: 'number_optional'; type: 'number'; required: false },
    { key: 'boolean_optional'; type: 'boolean'; required: false },
    { key: 'datetime_optional'; type: 'datetime'; required: false },
    { key: 'file_optional'; type: 'file'; required: false },
    { key: 'password_optional'; type: 'password'; required: false },
    { key: 'code_optional'; type: 'code'; required: false },
  ];
}>;
expectAssignable<multipleOptionalChildren>({});
expectAssignable<multipleOptionalChildren>(multipleOptionalChildNoLineItems);
expectAssignable<multipleOptionalChildren>({ parent_input: [{}] });
expectAssignable<multipleOptionalChildren>(multipleOptionalChildWithLineItems);
expectType<multipleOptionalChildren>(
  {} as {
    string_optional?: string;
    number_optional?: number;
    boolean_optional?: boolean;
    datetime_optional?: string;
    file_optional?: string;
    password_optional?: string;
    code_optional?: string;
    parent_input?: {
      string_optional?: string;
      number_optional?: number;
      boolean_optional?: boolean;
      datetime_optional?: string;
      file_optional?: string;
      password_optional?: string;
      code_optional?: string;
    }[];
  },
);

// Complete `children: [{...}]` case with complete mixture of required
// and optional fields.
const mixedOptionalChildrenNoLineItems = {
  string_required: 'some_string',
  string_optional: 'some_string',
  number_required: 1,
  number_optional: 1,
  boolean_required: true,
  boolean_optional: true,
  datetime_required: '2021-01-01T00:00:00Z',
  datetime_optional: '2021-01-01T00:00:00Z',
  file_required: 'some_file',
  file_optional: 'some_file',
  password_required: 'some_password',
  password_optional: 'some_password',
  code_required: 'some_code',
  code_optional: 'some_code',
};
const mixedOptionalChildrenWithLineItems = {
  string_required: 'some_string',
  string_optional: 'some_string',
  number_required: 1,
  number_optional: 1,
  boolean_required: true,
  boolean_optional: true,
  datetime_required: '2021-01-01T00:00:00Z',
  datetime_optional: '2021-01-01T00:00:00Z',
  file_required: 'some_file',
  file_optional: 'some_file',
  password_required: 'some_password',
  password_optional: 'some_password',
  code_required: 'some_code',
  code_optional: 'some_code',
  parent_input: [
    {
      string_required: 'item1',
      number_required: 2,
      boolean_required: false,
      datetime_required: '2022-01-01T00:00:00Z',
      file_required: 'file1',
      password_required: 'pass1',
      code_required: 'code1',
    },
  ],
};
type MixedOptionalChildren = PlainFieldContribution<{
  key: 'parent_input';
  children: [
    { key: 'string_required'; type: 'string'; required: true },
    { key: 'string_optional'; type: 'string'; required: false },
    { key: 'number_required'; type: 'number'; required: true },
    { key: 'number_optional'; type: 'number'; required: false },
    { key: 'boolean_required'; type: 'boolean'; required: true },
    { key: 'boolean_optional'; type: 'boolean'; required: false },
    { key: 'datetime_required'; type: 'datetime'; required: true },
    { key: 'datetime_optional'; type: 'datetime'; required: false },
    { key: 'file_required'; type: 'file'; required: true },
    { key: 'file_optional'; type: 'file'; required: false },
    { key: 'password_required'; type: 'password'; required: true },
    { key: 'password_optional'; type: 'password'; required: false },
    { key: 'code_required'; type: 'code'; required: true },
    { key: 'code_optional'; type: 'code'; required: false },
  ];
}>;
expectAssignable<MixedOptionalChildren>(mixedOptionalChildrenNoLineItems);
expectAssignable<MixedOptionalChildren>(mixedOptionalChildrenWithLineItems);
expectType<MixedOptionalChildren>(
  {} as {
    string_required: string;
    string_optional?: string;
    number_required: number;
    number_optional?: number;
    boolean_required: boolean;
    boolean_optional?: boolean;
    datetime_required: string;
    datetime_optional?: string;
    file_required: string;
    file_optional?: string;
    password_required: string;
    password_optional?: string;
    code_required: string;
    code_optional?: string;
    parent_input?: {
      string_required: string;
      string_optional?: string;
      number_required: number;
      number_optional?: number;
      boolean_required: boolean;
      boolean_optional?: boolean;
      datetime_required: string;
      datetime_optional?: string;
      file_required: string;
      file_optional?: string;
      password_required: string;
      password_optional?: string;
      code_required: string;
      code_optional?: string;
    }[];
  },
);

//
// Dictionary fields (where `dict:true` is set)
//
// Required dictionary fields.
const expectedDictRequired = {
  dict_required: {
    some_string_1: 'some_string_1',
  } as Record<string, string>,
};
type dictRequiredResult = PlainFieldContribution<{
  key: 'dict_required';
  type: 'string';
  required: true;
  dict: true;
}>;
expectType<dictRequiredResult>(expectedDictRequired);

// Optional dictionary fields.
type dictOptionalResult = PlainFieldContribution<{
  key: 'dict_optional';
  type: 'string';
  dict: true;
}>;
expectAssignable<dictOptionalResult>({
  dict_optional: { a: 'aaa' },
});
expectAssignable<dictOptionalResult>({
  dict_optional: {} as Record<string, string>,
});
expectAssignable<dictOptionalResult>({
  dict_optional: {},
});
expectAssignable<dictOptionalResult>({ dict_optional: undefined });
expectAssignable<dictOptionalResult>({});

//
// List fields (where `list:true` is set)
//
// Required string list fields.
const expectedStringListRequired = {
  list_required_string: ['some_string_1', 'some_string_2'],
};
type stringListRequiredResult = PlainFieldContribution<{
  key: 'list_required_string';
  type: 'string';
  required: true;
  list: true;
}>;
expectType<stringListRequiredResult>(expectedStringListRequired);

// Optional string list fields.
type stringListOptionalResult = PlainFieldContribution<{
  key: 'list_optional_string';
  type: 'string';
  list: true;
}>;
expectAssignable<stringListOptionalResult>({
  list_optional_string: ['some_string_1', 'some_string_2'],
});
expectAssignable<stringListOptionalResult>({
  list_optional_string: [],
});
expectAssignable<stringListOptionalResult>({
  list_optional_string: undefined,
});
expectAssignable<stringListOptionalResult>({});

// Optional omitted type (string) list fields.
type omittedTypeListOptionalResult = PlainFieldContribution<{
  key: 'list_optional_omitted_type';
  // No `type` set.
  list: true;
}>;
expectAssignable<omittedTypeListOptionalResult>({
  list_optional_omitted_type: ['some_string_1', 'some_string_2'],
});
expectAssignable<omittedTypeListOptionalResult>({
  list_optional_omitted_type: [],
});
expectAssignable<omittedTypeListOptionalResult>({
  list_optional_omitted_type: undefined,
});
expectAssignable<omittedTypeListOptionalResult>({});

// Required integer list fields.
const expectedIntegerListRequired = {
  list_required_integer: [1, 2],
};
type integerListRequiredResult = PlainFieldContribution<{
  key: 'list_required_integer';
  type: 'integer';
  required: true;
  list: true;
}>;
expectType<integerListRequiredResult>(expectedIntegerListRequired);

// Optional integer list fields.
type integerListOptionalResult = PlainFieldContribution<{
  key: 'list_required';
  type: 'integer';
  list: true;
}>;
expectAssignable<integerListOptionalResult>({
  list_required: [1, 2],
});
expectAssignable<integerListOptionalResult>({
  list_required: [],
});
expectAssignable<integerListOptionalResult>({
  list_required: undefined,
});
expectAssignable<integerListOptionalResult>({});

//
// Primitive fields (no parent, dict, or list. Single input field.)
//
// Required primitive string field.
const expectedPrimitiveRequired = {
  primitive_required_string: 'some_string',
};
type primitiveRequiredStringResult = PlainFieldContribution<{
  key: 'primitive_required_string';
  type: 'string';
  required: true;
}>;
expectType<primitiveRequiredStringResult>(expectedPrimitiveRequired);

// Optional primitive string field.
type primitiveOptionalStringResult = PlainFieldContribution<{
  key: 'primitive_optional_string';
  type: 'string';
}>;
expectAssignable<primitiveOptionalStringResult>({
  primitive_optional_string: 'some_string',
});
expectAssignable<primitiveOptionalStringResult>({
  primitive_optional_string: undefined,
});
expectAssignable<primitiveOptionalStringResult>({});

// Required primitive integer field.
const expectedPrimitiveRequiredInteger = {
  primitive_required_integer: 1,
};
type primitiveRequiredIntegerResult = PlainFieldContribution<{
  key: 'primitive_required_integer';
  type: 'integer';
  required: true;
}>;
expectType<primitiveRequiredIntegerResult>(expectedPrimitiveRequiredInteger);

// Optional primitive integer field.
type primitiveOptionalIntegerResult = PlainFieldContribution<{
  key: 'primitive_optional_integer';
  type: 'integer';
}>;
expectAssignable<primitiveOptionalIntegerResult>({
  primitive_optional_integer: 1,
});
expectAssignable<primitiveOptionalIntegerResult>({
  primitive_optional_integer: undefined,
});
expectAssignable<primitiveOptionalIntegerResult>({});

// Required primitive number field.
const expectedPrimitiveRequiredNumber = {
  primitive_required_number: 1.1,
};
type primitiveRequiredNumberResult = PlainFieldContribution<{
  key: 'primitive_required_number';
  type: 'number';
  required: true;
}>;
expectType<primitiveRequiredNumberResult>(expectedPrimitiveRequiredNumber);

// Optional primitive number field.
type primitiveOptionalNumberResult = PlainFieldContribution<{
  key: 'primitive_optional_number';
  type: 'number';
}>;
expectAssignable<primitiveOptionalNumberResult>({
  primitive_optional_number: 1.1,
});
expectAssignable<primitiveOptionalNumberResult>({
  primitive_optional_number: undefined,
});
expectAssignable<primitiveOptionalNumberResult>({});

// Required primitive boolean field.
const expectedPrimitiveRequiredBoolean = {
  primitive_required_boolean: true,
};
type primitiveRequiredBooleanResult = PlainFieldContribution<{
  key: 'primitive_required_boolean';
  type: 'boolean';
  required: true;
}>;
expectType<primitiveRequiredBooleanResult>(expectedPrimitiveRequiredBoolean);

// Optional primitive boolean field.
type primitiveOptionalBooleanResult = PlainFieldContribution<{
  key: 'primitive_optional_boolean';
  type: 'boolean';
}>;
expectAssignable<primitiveOptionalBooleanResult>({
  primitive_optional_boolean: true,
});
expectAssignable<primitiveOptionalBooleanResult>({
  primitive_optional_boolean: undefined,
});
expectAssignable<primitiveOptionalBooleanResult>({});

// Required primitive datetime field.
const expectedPrimitiveRequiredDatetime = {
  primitive_required_datetime: '2021-01-01T00:00:00Z',
};

type primitiveRequiredDatetimeResult = PlainFieldContribution<{
  key: 'primitive_required_datetime';
  type: 'datetime';
  required: true;
}>;
expectType<primitiveRequiredDatetimeResult>(expectedPrimitiveRequiredDatetime);

// Optional primitive datetime field.
type primitiveOptionalDatetimeResult = PlainFieldContribution<{
  key: 'primitive_optional_datetime';
  type: 'datetime';
}>;
expectAssignable<primitiveOptionalDatetimeResult>({
  primitive_optional_datetime: '2021-01-01T00:00:00Z',
});
expectAssignable<primitiveOptionalDatetimeResult>({
  primitive_optional_datetime: undefined,
});
expectAssignable<primitiveOptionalDatetimeResult>({});

// Optional primitive copy field.
type primitiveOptionalCopyResult = PlainFieldContribution<{
  key: 'primitive_optional_copy';
  type: 'copy';
}>;
expectAssignable<primitiveOptionalCopyResult>({
  primitive_optional_copy: undefined,
});
expectAssignable<primitiveOptionalCopyResult>({});

//
// Choices auto-complete unions.
//
// Required object choices auto-complete unions.
type requiredObjectChoicesAutoCompleteUnions = PlainFieldContribution<{
  key: 'required_object_choices_auto_complete_unions';
  type: 'string';
  required: true;
  choices: {
    alpha: 'Alpha';
    beta: 'Beta';
  };
}>;
expectType<requiredObjectChoicesAutoCompleteUnions>(
  {} as {
    required_object_choices_auto_complete_unions:
      | 'alpha'
      | 'beta'
      | (string & {});
  },
);
expectAssignable<requiredObjectChoicesAutoCompleteUnions>({
  required_object_choices_auto_complete_unions: 'alpha',
});

// Optional object choices auto-complete unions.
type optionalObjectChoicesAutoCompleteUnions = PlainFieldContribution<{
  key: 'optional_object_choices_auto_complete_unions';
  type: 'string';
  required: false;
  choices: {
    alpha: 'Alpha';
    beta: 'Beta';
  };
}>;
expectType<optionalObjectChoicesAutoCompleteUnions>(
  {} as {
    optional_object_choices_auto_complete_unions?:
      | 'alpha'
      | 'beta'
      | (string & {});
  },
);
expectAssignable<optionalObjectChoicesAutoCompleteUnions>({
  optional_object_choices_auto_complete_unions: 'alpha',
});
expectAssignable<optionalObjectChoicesAutoCompleteUnions>({
  optional_object_choices_auto_complete_unions: undefined,
});
expectAssignable<optionalObjectChoicesAutoCompleteUnions>({});

// Required array choices auto-complete unions.
type requiredFieldArrayChoicesAutoCompleteUnions = PlainFieldContribution<{
  key: 'required_field_array_choices_auto_complete_unions';
  type: 'string';
  required: true;
  choices: [
    { label: 'Alpha'; value: 'alpha'; sample: 'alpha' },
    { label: 'Beta'; value: 'beta'; sample: 'beta' },
  ];
}>;
expectType<requiredFieldArrayChoicesAutoCompleteUnions>(
  {} as {
    required_field_array_choices_auto_complete_unions:
      | 'alpha'
      | 'beta'
      | (string & {});
  },
);
expectAssignable<requiredFieldArrayChoicesAutoCompleteUnions>({
  required_field_array_choices_auto_complete_unions: 'alpha',
});
expectAssignable<requiredFieldArrayChoicesAutoCompleteUnions>({
  required_field_array_choices_auto_complete_unions: 'something_else',
});

// Optional array choices auto-complete unions.
type optionalFieldArrayChoicesAutoCompleteUnions = PlainFieldContribution<{
  key: 'optional_field_array_choices_auto_complete_unions';
  type: 'string';
  required: false;
  choices: [
    { label: 'Alpha'; value: 'alpha'; sample: 'alpha' },
    { label: 'Beta'; value: 'beta'; sample: 'beta' },
  ];
}>;
expectType<optionalFieldArrayChoicesAutoCompleteUnions>(
  {} as {
    optional_field_array_choices_auto_complete_unions?:
      | 'alpha'
      | 'beta'
      | (string & {});
  },
);
expectAssignable<optionalFieldArrayChoicesAutoCompleteUnions>({
  optional_field_array_choices_auto_complete_unions: 'alpha',
});
expectAssignable<optionalFieldArrayChoicesAutoCompleteUnions>({});

//
// Required string array choices auto-complete unions.
type requiredStringArrayChoicesAutoCompleteUnions = PlainFieldContribution<{
  key: 'required_string_array_choices_auto_complete_unions';
  type: 'string';
  required: true;
  choices: ['alpha', 'beta'];
}>;
expectType<requiredStringArrayChoicesAutoCompleteUnions>(
  {} as {
    required_string_array_choices_auto_complete_unions:
      | 'alpha'
      | 'beta'
      | (string & {});
  },
);
expectAssignable<requiredStringArrayChoicesAutoCompleteUnions>({
  required_string_array_choices_auto_complete_unions: 'alpha',
});
expectAssignable<requiredStringArrayChoicesAutoCompleteUnions>({
  required_string_array_choices_auto_complete_unions: 'something_else',
});

type optionalStringArrayChoicesAutoCompleteUnions = PlainFieldContribution<{
  key: 'optional_string_array_choices_auto_complete_unions';
  type: 'string';
  required: false;
  choices: ['alpha', 'beta'];
}>;
expectType<optionalStringArrayChoicesAutoCompleteUnions>(
  {} as {
    optional_string_array_choices_auto_complete_unions?:
      | 'alpha'
      | 'beta'
      | (string & {});
  },
);
expectAssignable<optionalStringArrayChoicesAutoCompleteUnions>({
  optional_string_array_choices_auto_complete_unions: 'alpha',
});
expectAssignable<optionalStringArrayChoicesAutoCompleteUnions>({});

//
// JSON fields (where `type: 'json'` is set)
//

// Required json primitive field.
type primitiveRequiredJsonResult = PlainFieldContribution<{
  key: 'primitive_required_json';
  type: 'json';
  required: true;
}>;
expectType<primitiveRequiredJsonResult>(
  {} as { primitive_required_json: JsonFieldValue },
);
expectAssignable<primitiveRequiredJsonResult>({
  primitive_required_json: { name: 'test' },
});
expectAssignable<primitiveRequiredJsonResult>({
  primitive_required_json: ['a', 'b'],
});

// Optional json primitive field.
type primitiveOptionalJsonResult = PlainFieldContribution<{
  key: 'primitive_optional_json';
  type: 'json';
}>;
expectAssignable<primitiveOptionalJsonResult>({
  primitive_optional_json: { nested: { deep: true } },
});
expectAssignable<primitiveOptionalJsonResult>({
  primitive_optional_json: [1, 2, 3],
});
expectAssignable<primitiveOptionalJsonResult>({
  primitive_optional_json: undefined,
});
expectAssignable<primitiveOptionalJsonResult>({});

// Required json list field.
type jsonListRequiredResult = PlainFieldContribution<{
  key: 'list_required_json';
  type: 'json';
  required: true;
  list: true;
}>;
expectType<jsonListRequiredResult>(
  {} as { list_required_json: JsonFieldValue[] },
);
expectAssignable<jsonListRequiredResult>({
  list_required_json: [{ a: 1 }, { b: 2 }],
});

// Json field with schema — infers specific type from schema.
type jsonWithSchemaResult = PlainFieldContribution<{
  key: 'payload';
  type: 'json';
  required: true;
  schema: {
    type: 'object';
    properties: {
      name: { type: 'string' };
      age: { type: 'number' };
    };
    required: ['name'];
    additionalProperties: false;
  };
}>;
expectType<jsonWithSchemaResult>(
  {} as { payload: { name: string; age?: number } },
);

// Json field with schema, optional.
type jsonWithSchemaOptionalResult = PlainFieldContribution<{
  key: 'data';
  type: 'json';
  schema: {
    type: 'object';
    properties: {
      items: { type: 'array'; items: { type: 'string' } };
    };
  };
}>;
expectAssignable<jsonWithSchemaOptionalResult>({
  data: { items: ['a', 'b'] },
});
expectAssignable<jsonWithSchemaOptionalResult>({});

// Json field with schema + list.
type jsonWithSchemaListResult = PlainFieldContribution<{
  key: 'payloads';
  type: 'json';
  required: true;
  list: true;
  schema: {
    type: 'object';
    properties: {
      id: { type: 'number' };
    };
    additionalProperties: false;
  };
}>;
expectType<jsonWithSchemaListResult>(
  {} as { payloads: { id?: number }[] },
);

// Json field WITHOUT schema still falls back to JsonFieldValue.
type jsonNoSchemaResult = PlainFieldContribution<{
  key: 'raw';
  type: 'json';
  required: true;
}>;
expectType<jsonNoSchemaResult>(
  {} as { raw: JsonFieldValue },
);

// Json field as a child of a parent field.
type jsonChildResult = PlainFieldContribution<{
  key: 'parent_with_json';
  children: [
    { key: 'json_child'; type: 'json'; required: true },
    { key: 'string_child'; type: 'string'; required: true },
  ];
}>;
expectAssignable<jsonChildResult>({
  json_child: { key: 'value' },
  string_child: 'hello',
});
expectType<jsonChildResult>(
  {} as {
    json_child: JsonFieldValue;
    string_child: string;
    parent_with_json?: {
      json_child: JsonFieldValue;
      string_child: string;
    }[];
  },
);
