import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { DragDropUpload } from './DragDropUpload';

function createMockFile(name: string, size: number, type: string): File {
  const content = new Array(size).fill('a').join('');
  return new File([content], name, { type });
}

function createMockXHR(options: {
  status?: number;
  response?: object;
  progressEvents?: { loaded: number; total: number }[];
  shouldError?: boolean;
  shouldTimeout?: boolean;
} = {}) {
  const {
    status = 200,
    response = { artifact_id: 'abc123', size: 100 },
    progressEvents = [],
    shouldError = false,
    shouldTimeout = false,
  } = options;

  return class MockXHR {
    status = status;
    responseText = JSON.stringify(response);
    timeout = 0;
    upload = {
      addEventListener: vi.fn((event: string, handler: (e: ProgressEvent) => void) => {
        if (event === 'progress') {
          progressEvents.forEach((p, i) => {
            setTimeout(() => {
              handler({ lengthComputable: true, loaded: p.loaded, total: p.total } as ProgressEvent);
            }, i * 10);
          });
        }
      }),
    };
    addEventListener = vi.fn((event: string, handler: () => void) => {
      if (event === 'load' && !shouldError && !shouldTimeout) {
        setTimeout(handler, progressEvents.length * 10 + 10);
      }
      if (event === 'error' && shouldError) {
        setTimeout(handler, 10);
      }
      if (event === 'timeout' && shouldTimeout) {
        setTimeout(handler, 10);
      }
    });
    open = vi.fn();
    send = vi.fn();
  };
}

describe('DragDropUpload', () => {
  const defaultProps = {
    projectName: 'test-project',
    packageName: 'test-package',
  };

  beforeEach(() => {
    vi.useFakeTimers({ shouldAdvanceTime: true });
  });

  afterEach(() => {
    vi.useRealTimers();
    vi.restoreAllMocks();
  });

  describe('Rendering', () => {
    it('renders drop zone with instructional text', () => {
      render(<DragDropUpload {...defaultProps} />);
      
      expect(screen.getByText(/drag files here/i)).toBeInTheDocument();
      expect(screen.getByText(/click to browse/i)).toBeInTheDocument();
    });

    it('renders hidden file input', () => {
      render(<DragDropUpload {...defaultProps} />);
      
      const input = document.querySelector('input[type="file"]');
      expect(input).toBeInTheDocument();
      expect(input).toHaveClass('drop-zone__input');
    });

    it('shows max file size hint when provided', () => {
      render(<DragDropUpload {...defaultProps} maxFileSize={1024 * 1024} />);
      
      expect(screen.getByText(/max file size: 1 mb/i)).toBeInTheDocument();
    });

    it('shows allowed types hint when provided', () => {
      render(<DragDropUpload {...defaultProps} allowedTypes={['.zip', '.tar.gz']} allowAllTypes={false} />);
      
      expect(screen.getByText(/\.zip, \.tar\.gz/i)).toBeInTheDocument();
    });
  });

  describe('Click to Browse', () => {
    it('opens file picker when drop zone is clicked', async () => {
      render(<DragDropUpload {...defaultProps} />);
      
      const input = document.querySelector('input[type="file"]') as HTMLInputElement;
      const clickSpy = vi.spyOn(input, 'click');
      
      const dropZone = screen.getByRole('button');
      await userEvent.click(dropZone);
      
      expect(clickSpy).toHaveBeenCalled();
    });

    it('opens file picker on Enter key', () => {
      render(<DragDropUpload {...defaultProps} />);
      
      const input = document.querySelector('input[type="file"]') as HTMLInputElement;
      const clickSpy = vi.spyOn(input, 'click');
      
      const dropZone = screen.getByRole('button');
      fireEvent.keyDown(dropZone, { key: 'Enter' });
      
      expect(clickSpy).toHaveBeenCalled();
    });
  });

  describe('Drag and Drop Events', () => {
    it('shows visual feedback on drag over', () => {
      render(<DragDropUpload {...defaultProps} />);
      
      const dropZone = screen.getByRole('button');
      
      fireEvent.dragEnter(dropZone, {
        dataTransfer: { items: [{}] },
      });
      
      expect(dropZone).toHaveClass('drop-zone--active');
    });

    it('removes visual feedback on drag leave', () => {
      render(<DragDropUpload {...defaultProps} />);
      
      const dropZone = screen.getByRole('button');
      
      fireEvent.dragEnter(dropZone, { dataTransfer: { items: [{}] } });
      expect(dropZone).toHaveClass('drop-zone--active');
      
      fireEvent.dragLeave(dropZone);
      expect(dropZone).not.toHaveClass('drop-zone--active');
    });

    it('accepts dropped files', async () => {
      const MockXHR = createMockXHR();
      vi.stubGlobal('XMLHttpRequest', MockXHR);
      
      render(<DragDropUpload {...defaultProps} />);
      
      const dropZone = screen.getByRole('button');
      const file = createMockFile('test.txt', 100, 'text/plain');
      
      const dataTransfer = new DataTransfer();
      Object.defineProperty(dataTransfer, 'files', {
        value: Object.assign([file], { item: (i: number) => [file][i] }),
      });
      
      fireEvent.drop(dropZone, { dataTransfer });
      
      await waitFor(() => {
        expect(screen.getByText('test.txt')).toBeInTheDocument();
      });
    });
  });

  describe('File Validation', () => {
    it('rejects files exceeding max size', async () => {
      render(<DragDropUpload {...defaultProps} maxFileSize={100} />);
      
      const input = document.querySelector('input[type="file"]') as HTMLInputElement;
      const file = createMockFile('large.txt', 200, 'text/plain');
      
      Object.defineProperty(input, 'files', {
        value: Object.assign([file], { item: (i: number) => [file][i] }),
      });
      
      fireEvent.change(input);
      
      await waitFor(() => {
        expect(screen.getByText(/exceeds.*limit/i)).toBeInTheDocument();
      });
    });

    it('rejects files with invalid type when allowAllTypes is false', async () => {
      render(<DragDropUpload {...defaultProps} allowedTypes={['.zip']} allowAllTypes={false} />);
      
      const input = document.querySelector('input[type="file"]') as HTMLInputElement;
      const file = createMockFile('test.txt', 100, 'text/plain');
      
      Object.defineProperty(input, 'files', {
        value: Object.assign([file], { item: (i: number) => [file][i] }),
      });
      
      fireEvent.change(input);
      
      await waitFor(() => {
        expect(screen.getByText(/not allowed/i)).toBeInTheDocument();
      });
    });

    it('rejects empty files', async () => {
      render(<DragDropUpload {...defaultProps} />);
      
      const input = document.querySelector('input[type="file"]') as HTMLInputElement;
      const file = createMockFile('empty.txt', 0, 'text/plain');
      
      Object.defineProperty(input, 'files', {
        value: Object.assign([file], { item: (i: number) => [file][i] }),
      });
      
      fireEvent.change(input);
      
      await waitFor(() => {
        expect(screen.getByText(/empty file/i)).toBeInTheDocument();
      });
    });

    it('accepts valid files when allowAllTypes is true', async () => {
      const MockXHR = createMockXHR();
      vi.stubGlobal('XMLHttpRequest', MockXHR);
      
      render(<DragDropUpload {...defaultProps} allowAllTypes={true} />);
      
      const input = document.querySelector('input[type="file"]') as HTMLInputElement;
      const file = createMockFile('test.txt', 100, 'text/plain');
      
      Object.defineProperty(input, 'files', {
        value: Object.assign([file], { item: (i: number) => [file][i] }),
      });
      
      fireEvent.change(input);
      
      await waitFor(() => {
        expect(screen.getByText('test.txt')).toBeInTheDocument();
        expect(screen.queryByText(/not allowed/i)).not.toBeInTheDocument();
      });
    });
  });

  describe('Upload Queue', () => {
    it('shows file in queue after selection', async () => {
      const MockXHR = createMockXHR();
      vi.stubGlobal('XMLHttpRequest', MockXHR);
      
      render(<DragDropUpload {...defaultProps} />);
      
      const input = document.querySelector('input[type="file"]') as HTMLInputElement;
      const file = createMockFile('document.pdf', 1024, 'application/pdf');
      
      Object.defineProperty(input, 'files', {
        value: Object.assign([file], { item: (i: number) => [file][i] }),
      });
      
      fireEvent.change(input);
      
      await waitFor(() => {
        expect(screen.getByText('document.pdf')).toBeInTheDocument();
        expect(screen.getByText('1 KB')).toBeInTheDocument();
      });
    });

    it('handles multiple files', async () => {
      const MockXHR = createMockXHR();
      vi.stubGlobal('XMLHttpRequest', MockXHR);
      
      render(<DragDropUpload {...defaultProps} />);
      
      const input = document.querySelector('input[type="file"]') as HTMLInputElement;
      const files = [
        createMockFile('file1.txt', 100, 'text/plain'),
        createMockFile('file2.txt', 200, 'text/plain'),
        createMockFile('file3.txt', 300, 'text/plain'),
      ];
      
      Object.defineProperty(input, 'files', {
        value: Object.assign(files, { item: (i: number) => files[i] }),
      });
      
      fireEvent.change(input);
      
      await waitFor(() => {
        expect(screen.getByText('file1.txt')).toBeInTheDocument();
        expect(screen.getByText('file2.txt')).toBeInTheDocument();
        expect(screen.getByText('file3.txt')).toBeInTheDocument();
      });
    });

    it('shows overall progress for multiple files', async () => {
      const MockXHR = createMockXHR();
      vi.stubGlobal('XMLHttpRequest', MockXHR);
      
      render(<DragDropUpload {...defaultProps} />);
      
      const input = document.querySelector('input[type="file"]') as HTMLInputElement;
      const files = [
        createMockFile('file1.txt', 100, 'text/plain'),
        createMockFile('file2.txt', 100, 'text/plain'),
      ];
      
      Object.defineProperty(input, 'files', {
        value: Object.assign(files, { item: (i: number) => files[i] }),
      });
      
      fireEvent.change(input);
      
      await waitFor(() => {
        expect(screen.getByText(/uploading.*of.*files/i)).toBeInTheDocument();
      });
    });
  });

  describe('Upload Progress', () => {
    it('shows progress bar during upload', async () => {
      const MockXHR = createMockXHR({
        progressEvents: [
          { loaded: 50, total: 100 },
        ],
      });
      vi.stubGlobal('XMLHttpRequest', MockXHR);
      
      render(<DragDropUpload {...defaultProps} />);
      
      const input = document.querySelector('input[type="file"]') as HTMLInputElement;
      const file = createMockFile('test.txt', 100, 'text/plain');
      
      Object.defineProperty(input, 'files', {
        value: Object.assign([file], { item: (i: number) => [file][i] }),
      });
      
      fireEvent.change(input);
      
      await waitFor(() => {
        const progressBar = document.querySelector('.progress-bar__fill');
        expect(progressBar).toBeInTheDocument();
      });
    });
  });

  describe('Upload Completion', () => {
    it('shows success state when upload completes', async () => {
      const MockXHR = createMockXHR({
        response: { artifact_id: 'abc123def456', size: 100 },
      });
      vi.stubGlobal('XMLHttpRequest', MockXHR);
      
      const onComplete = vi.fn();
      render(<DragDropUpload {...defaultProps} onUploadComplete={onComplete} />);
      
      const input = document.querySelector('input[type="file"]') as HTMLInputElement;
      const file = createMockFile('test.txt', 100, 'text/plain');
      
      Object.defineProperty(input, 'files', {
        value: Object.assign([file], { item: (i: number) => [file][i] }),
      });
      
      fireEvent.change(input);
      
      await vi.advanceTimersByTimeAsync(100);
      
      await waitFor(() => {
        expect(screen.getByText(/abc123def456/i)).toBeInTheDocument();
      });
    });

    it('calls onUploadComplete callback with results', async () => {
      const MockXHR = createMockXHR({
        response: { artifact_id: 'test-artifact-id', size: 100 },
      });
      vi.stubGlobal('XMLHttpRequest', MockXHR);
      
      const onComplete = vi.fn();
      render(<DragDropUpload {...defaultProps} onUploadComplete={onComplete} />);
      
      const input = document.querySelector('input[type="file"]') as HTMLInputElement;
      const file = createMockFile('test.txt', 100, 'text/plain');
      
      Object.defineProperty(input, 'files', {
        value: Object.assign([file], { item: (i: number) => [file][i] }),
      });
      
      fireEvent.change(input);
      
      await vi.advanceTimersByTimeAsync(100);
      
      await waitFor(() => {
        expect(onComplete).toHaveBeenCalledWith([
          expect.objectContaining({ artifact_id: 'test-artifact-id' }),
        ]);
      });
    });
  });

  describe('Upload Errors', () => {
    it('shows error state when upload fails after retries exhausted', async () => {
      const MockXHR = createMockXHR({
        status: 500,
        response: { detail: 'Server error' },
        shouldError: true,
      });
      vi.stubGlobal('XMLHttpRequest', MockXHR);
      
      render(<DragDropUpload {...defaultProps} maxRetries={0} />);
      
      const input = document.querySelector('input[type="file"]') as HTMLInputElement;
      const file = createMockFile('test.txt', 100, 'text/plain');
      
      Object.defineProperty(input, 'files', {
        value: Object.assign([file], { item: (i: number) => [file][i] }),
      });
      
      fireEvent.change(input);
      
      await vi.advanceTimersByTimeAsync(100);
      
      await waitFor(() => {
        expect(screen.getByText(/network error/i)).toBeInTheDocument();
      });
    });

    it('calls onUploadError callback when retries exhausted', async () => {
      const MockXHR = createMockXHR({ shouldError: true });
      vi.stubGlobal('XMLHttpRequest', MockXHR);
      
      const onError = vi.fn();
      render(<DragDropUpload {...defaultProps} maxRetries={0} onUploadError={onError} />);
      
      const input = document.querySelector('input[type="file"]') as HTMLInputElement;
      const file = createMockFile('test.txt', 100, 'text/plain');
      
      Object.defineProperty(input, 'files', {
        value: Object.assign([file], { item: (i: number) => [file][i] }),
      });
      
      fireEvent.change(input);
      
      await vi.advanceTimersByTimeAsync(100);
      
      await waitFor(() => {
        expect(onError).toHaveBeenCalled();
      });
    });
  });

  describe('Queue Actions', () => {
    it('removes item from queue when remove button clicked', async () => {
      const MockXHR = createMockXHR();
      vi.stubGlobal('XMLHttpRequest', MockXHR);
      
      render(<DragDropUpload {...defaultProps} />);
      
      const input = document.querySelector('input[type="file"]') as HTMLInputElement;
      const file = createMockFile('test.txt', 100, 'text/plain');
      
      Object.defineProperty(input, 'files', {
        value: Object.assign([file], { item: (i: number) => [file][i] }),
      });
      
      fireEvent.change(input);
      
      await vi.advanceTimersByTimeAsync(100);
      
      await waitFor(() => {
        expect(screen.getByText('test.txt')).toBeInTheDocument();
      });
      
      const removeButton = screen.getByTitle('Remove');
      fireEvent.click(removeButton);
      
      await waitFor(() => {
        expect(screen.queryByText('test.txt')).not.toBeInTheDocument();
      });
    });

    it('clears completed items when clear button clicked', async () => {
      const MockXHR = createMockXHR();
      vi.stubGlobal('XMLHttpRequest', MockXHR);
      
      render(<DragDropUpload {...defaultProps} />);
      
      const input = document.querySelector('input[type="file"]') as HTMLInputElement;
      const file = createMockFile('test.txt', 100, 'text/plain');
      
      Object.defineProperty(input, 'files', {
        value: Object.assign([file], { item: (i: number) => [file][i] }),
      });
      
      fireEvent.change(input);
      
      await vi.advanceTimersByTimeAsync(100);
      
      await waitFor(() => {
        const clearButton = screen.queryByText(/clear finished/i);
        if (clearButton) {
          fireEvent.click(clearButton);
        }
      });
    });
  });

  describe('Tag Support', () => {
    it('includes tag in upload request', async () => {
      let capturedFormData: FormData | null = null;
      
      class MockXHR {
        status = 200;
        responseText = JSON.stringify({ artifact_id: 'abc123', size: 100 });
        timeout = 0;
        upload = { addEventListener: vi.fn() };
        addEventListener = vi.fn((event: string, handler: () => void) => {
          if (event === 'load') setTimeout(handler, 10);
        });
        open = vi.fn();
        send = vi.fn((data: FormData) => {
          capturedFormData = data;
        });
      }
      vi.stubGlobal('XMLHttpRequest', MockXHR);
      
      render(<DragDropUpload {...defaultProps} tag="v1.0.0" />);
      
      const input = document.querySelector('input[type="file"]') as HTMLInputElement;
      const file = createMockFile('test.txt', 100, 'text/plain');
      
      Object.defineProperty(input, 'files', {
        value: Object.assign([file], { item: (i: number) => [file][i] }),
      });
      
      fireEvent.change(input);
      
      await vi.advanceTimersByTimeAsync(100);
      
      await waitFor(() => {
        expect(capturedFormData?.get('tag')).toBe('v1.0.0');
      });
    });
  });
});
