import { describe, it, expect, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import MFileUploader, {
  type FilesValidationState,
  type FileUploaderProps,
} from './MFileUploader.vue';
import { nextTick } from 'vue';

type WrapperVm = {
  dragCounter: number;
};

const globalStubs = {
  MFileUploaderItem: {
    template: '<div class="file-item"><slot name="action" /></div>',
    name: 'MFileUploaderItem',
    props: ['file', 'valid'],
    emits: ['delete'],
  },
  Upload24: true,
};

const createFile = (name: string, size: number, type: string) => {
  const file = new File(['a'.repeat(size)], name, { type });
  Object.defineProperty(file, 'size', { value: size });
  return file;
};

const mountUploader = (props = {} as FileUploaderProps) =>
  mount(MFileUploader, { props, global: { stubs: globalStubs } });

const triggerFileInputChange = async (
  wrapper: ReturnType<typeof mount>,
  files: File[],
) => {
  const input = wrapper.find('input[type="file"]');
  Object.defineProperty(input.element, 'files', {
    value: files,
    configurable: true,
  });
  await input.trigger('change');
};

const triggerDrop = async (
  wrapper: ReturnType<typeof mount>,
  files: File[],
) => {
  const dropZone = wrapper.find('.mc-file-uploader__input');
  await dropZone.trigger('drop', {
    dataTransfer: {
      files,
      items: files.map((f) => ({
        kind: 'file',
        type: f.type,
        getAsFile: () => f,
      })),
    },
    preventDefault: vi.fn(),
    stopPropagation: vi.fn(),
  });
};

describe('MFileUploader.vue', () => {
  it('allows file upload via input', async () => {
    const wrapper = mountUploader({
      modelValue: [],
    });
    const file = createFile('test.png', 1024, 'image/png');
    await triggerFileInputChange(wrapper, [file]);
    expect(wrapper.emitted('update:modelValue')).toBeTruthy();
  });

  it('clears input and returns if no new valid files', async () => {
    const existingFile = createFile('exist.png', 100, 'image/png');
    const wrapper = mountUploader({
      modelValue: [existingFile],
    });

    const input = wrapper.find('input[type="file"]');
    Object.defineProperty(input.element, 'files', {
      value: [],
      configurable: true,
    });

    await input.trigger('change');

    expect((input.element as HTMLInputElement).value).toBe('');
    expect(wrapper.emitted('update:modelValue')).toBeFalsy();
  });

  it.each([
    ['Enter', 'Enter'],
    ['Space', ' '],
  ])('clicks file input when %s is pressed', async (_, key) => {
    const wrapper = mountUploader({
      modelValue: [],
    });
    const inputEl = wrapper.find('input[type="file"]')
      .element as HTMLInputElement;
    const clickSpy = vi.spyOn(inputEl, 'click');
    const label = wrapper.find('.mc-file-uploader__input');
    await label.trigger('keydown', { key });
    expect(clickSpy).toHaveBeenCalled();
  });

  it('allows drag and drop', async () => {
    const wrapper = mountUploader({
      modelValue: [],
      hasDragDrop: true,
    });
    const file = createFile('dragged.pdf', 500, 'application/pdf');
    await triggerDrop(wrapper, [file]);
    expect(wrapper.emitted('update:modelValue')).toBeTruthy();
  });

  it('handles dragenter/dragleave class correctly', async () => {
    const wrapper = mountUploader({
      modelValue: [],
      hasDragDrop: true,
    });
    const labelEl = wrapper.get('.mc-file-uploader__input');

    await labelEl.trigger('dragenter');
    expect(labelEl.classes()).toContain('mc-file-uploader__input--dragged');

    await labelEl.trigger('dragleave');
    expect(labelEl.classes()).not.toContain('mc-file-uploader__input--dragged');
  });

  it('does not update on drop if disabled or hasDragDrop is false', async () => {
    const wrapperFalse = mountUploader({
      modelValue: [],
      hasDragDrop: false,
    });
    await triggerDrop(wrapperFalse, [
      createFile('a.pdf', 100, 'application/pdf'),
    ]);
    expect(wrapperFalse.emitted('update:modelValue')).toBeFalsy();

    const wrapperDisabled = mountUploader({
      modelValue: [],
      hasDragDrop: true,
      disabled: true,
    });
    await triggerDrop(wrapperDisabled, [
      createFile('b.pdf', 100, 'application/pdf'),
    ]);
    expect(wrapperDisabled.emitted('update:modelValue')).toBeFalsy();
  });

  it('does not update dragCounter on dragEnter if hasDragDrop is false', async () => {
    const buttonWrapper = mountUploader({
      modelValue: [],
      hasDragDrop: false,
    });

    const vm = buttonWrapper.vm as unknown as WrapperVm;

    vm.dragCounter = 1;
    const input = buttonWrapper.find('.mc-file-uploader__input');
    await input.trigger('dragenter');
    expect(vm.dragCounter).toBe(1);
  });

  it('does not update dragCounter on dragLeave if hasDragDrop is false', async () => {
    const buttonWrapper = mountUploader({
      modelValue: [],
      hasDragDrop: false,
    });

    const vm = buttonWrapper.vm as unknown as WrapperVm;

    vm.dragCounter = 1;
    const input = buttonWrapper.find('.mc-file-uploader__input');
    await input.trigger('dragleave');
    expect(vm.dragCounter).toBe(1);
  });

  it('adds new files on change when multiple = true', async () => {
    const file1 = createFile('a.txt', 1, 'text/plain');
    const file2 = createFile('b.txt', 1, 'text/plain');

    const wrapper = mountUploader({
      modelValue: [file1],
      multiple: true,
    });

    const input = wrapper.find('input[type="file"]');
    Object.defineProperty(input.element, 'files', {
      value: [file2],
      configurable: true,
    });

    await input.trigger('change');

    const updates = wrapper.emitted('update:modelValue')![0][0] as File[];
    expect(updates.map((f) => f.name)).toEqual(['a.txt', 'b.txt']);
    expect((input.element as HTMLInputElement).value).toBe('');
  });

  it('replaces existing file on change when multiple = false', async () => {
    const file1 = createFile('a.txt', 1, 'text/plain');
    const file2 = createFile('b.txt', 1, 'text/plain');

    const wrapper = mountUploader({
      modelValue: [file1],
      multiple: false,
    });

    const input = wrapper.find('input[type="file"]');
    Object.defineProperty(input.element, 'files', {
      value: [file2],
      configurable: true,
    });

    await input.trigger('change');

    const updates = wrapper.emitted('update:modelValue')![0][0] as File[];
    expect(updates.map((f) => f.name)).toEqual(['b.txt']);
    expect((input.element as HTMLInputElement).value).toBe('');
  });

  it('merges or replaces files on drop based on multiple prop', async () => {
    const file1 = createFile('a.txt', 1, 'text/plain');
    const file2 = createFile('b.txt', 1, 'text/plain');

    // multiple = true
    const wrapperMulti = mountUploader({
      modelValue: [file1],
      multiple: true,
      hasDragDrop: true,
    });
    await triggerDrop(wrapperMulti, [file2]);
    const payloadMulti = wrapperMulti.emitted(
      'update:modelValue',
    )![0][0] as File[];
    expect(payloadMulti.map((f) => f.name)).toEqual(['a.txt', 'b.txt']);

    // multiple = false
    const wrapperSingle = mountUploader({
      modelValue: [file1],
      multiple: false,
    });
    await triggerFileInputChange(wrapperSingle, [file2]);
    const payloadSingle = wrapperSingle.emitted(
      'update:modelValue',
    )![0][0] as File[];
    expect(payloadSingle.map((f) => f.name)).toEqual(['b.txt']);
  });

  describe('Validation', () => {
    it('validates size, extension and custom rules', async () => {
      const valid = createFile('good.png', 1000, 'image/png');
      const heavy = createFile('heavy.png', 5000, 'image/png');
      const wrong = createFile('wrong.txt', 1000, 'text/plain');
      const customRule = vi.fn().mockReturnValue(false);

      const wrapper = mountUploader({
        modelValue: [
          valid,
          heavy,
          wrong,
          createFile('test.png', 100, 'image/png'),
        ],
        maxSize: 2000,
        allowedExtensions: ['png', 'jpg'],
        rules: [customRule],
      });

      const emitted = wrapper.emitted(
        'validation',
      )![0][0] as FilesValidationState;
      expect(emitted['good.png'].size).toBe(true);
      expect(emitted['heavy.png'].size).toBe(false);
      expect(emitted['test.png'].customValidation).toBe(false);
    });
  });

  it('handles file deletion', async () => {
    const f1 = createFile('1.png', 100, 'image/png');
    const f2 = createFile('2.png', 100, 'image/png');
    const wrapper = mountUploader({
      modelValue: [f1, f2],
      showFilesList: true,
    });

    const items = wrapper.findAllComponents({ name: 'MFileUploaderItem' });
    await items[0].vm.$emit('delete');
    await nextTick();

    const payload = wrapper.emitted('update:modelValue')![0][0] as File[];
    expect(payload.map((f) => f.name)).toEqual(['2.png']);
  });

  it('disables input when disabled=true', () => {
    const wrapper = mountUploader({
      disabled: true,
      modelValue: [],
    });
    expect(
      wrapper.find('input[type="file"]').attributes('disabled'),
    ).toBeDefined();
  });
});
