import React from 'react';
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
import FileDropZone from '../src/components/FileDropZone';
import { sessionFilesApi } from '../src/api/client';

jest.mock('../src/api/client', () => ({
  sessionFilesApi: {
    list:   jest.fn(),
    create: jest.fn(),
    delete: jest.fn(),
  },
}));

// ── Fixtures ───────────────────────────────────────────────────────────────────

const DATE = '2026-04-23';

const ANIMALS = [
  { id: 'a1', animal_name: 'Rat 2852', animal_id_string: '2852' },
  { id: 'a2', animal_name: 'Rat 3076', animal_id_string: '3076' },
  { id: 'a3', animal_name: 'Rat 3077', animal_id_string: '3077' },
];

const STATUS_A1 = { id: 's-a1', animal_id: 'a1', date: `${DATE}T00:00:00.000Z` };
const STATUS_A2 = { id: 's-a2', animal_id: 'a2', date: `${DATE}T00:00:00.000Z` };
const STATUS_A3 = { id: 's-a3', animal_id: 'a3', date: `${DATE}T00:00:00.000Z` };

const ROWS = [
  { animal: ANIMALS[0], status: STATUS_A1 },
  { animal: ANIMALS[1], status: STATUS_A2 },
  { animal: ANIMALS[2], status: STATUS_A3 },
];

function makeFile(name, size = 1024, type = '') {
  return new File(['x'], name, { type, lastModified: Date.now() });
}

function makeSavedFile(id, statusId, filename, fileSize = '10240') {
  return { id, daily_status_id: statusId, filename, file_size: fileSize, file_type: 'CSV', last_modified: null, created_at: new Date().toISOString() };
}

function dropFiles(files) {
  const zone = screen.getByTestId('drop-zone');
  fireEvent.dragEnter(zone, { dataTransfer: { files } });
  fireEvent.dragOver(zone, { dataTransfer: { files } });
  fireEvent.drop(zone, { dataTransfer: { files } });
}

beforeEach(() => {
  jest.clearAllMocks();
  sessionFilesApi.list.mockResolvedValue([]);
  sessionFilesApi.create.mockResolvedValue([]);
  sessionFilesApi.delete.mockResolvedValue();
});

// ── Rendering ──────────────────────────────────────────────────────────────────

describe('rendering', () => {
  it('renders the drop zone', () => {
    render(<FileDropZone rows={ROWS} date={DATE} />);
    expect(screen.getByTestId('drop-zone')).toBeInTheDocument();
    expect(screen.getByText(/drag.*drop session files/i)).toBeInTheDocument();
  });

  it('shows naming convention hint using first animal id', () => {
    render(<FileDropZone rows={ROWS} date={DATE} />);
    expect(screen.getByText(/2026-04-23_2852/)).toBeInTheDocument();
  });

  it('loads saved files on mount for each status', async () => {
    sessionFilesApi.list.mockResolvedValue([]);
    render(<FileDropZone rows={ROWS} date={DATE} />);
    await waitFor(() => {
      expect(sessionFilesApi.list).toHaveBeenCalledWith('s-a1');
      expect(sessionFilesApi.list).toHaveBeenCalledWith('s-a2');
      expect(sessionFilesApi.list).toHaveBeenCalledWith('s-a3');
    });
  });

  it('shows saved files loaded from the API', async () => {
    sessionFilesApi.list.mockImplementation((id) =>
      id === 's-a1'
        ? Promise.resolve([makeSavedFile('f1', 's-a1', `${DATE}_2852_trials.csv`)])
        : Promise.resolve([])
    );
    render(<FileDropZone rows={ROWS} date={DATE} />);
    await waitFor(() =>
      expect(screen.getByText(`${DATE}_2852_trials.csv`)).toBeInTheDocument()
    );
    expect(screen.getByTestId('saved-files')).toBeInTheDocument();
  });
});

// ── Drag state ─────────────────────────────────────────────────────────────────

describe('drag interaction', () => {
  it('highlights drop zone on dragenter', () => {
    render(<FileDropZone rows={ROWS} date={DATE} />);
    const zone = screen.getByTestId('drop-zone');
    fireEvent.dragEnter(zone, { dataTransfer: { files: [] } });
    expect(zone.className).toMatch(/border-blue-400/);
    expect(screen.getByText(/release to register/i)).toBeInTheDocument();
  });

  it('removes highlight on dragleave', () => {
    render(<FileDropZone rows={ROWS} date={DATE} />);
    const zone = screen.getByTestId('drop-zone');
    fireEvent.dragEnter(zone, { dataTransfer: { files: [] } });
    fireEvent.dragLeave(zone, { dataTransfer: { files: [] } });
    expect(zone.className).not.toMatch(/border-blue-400/);
  });
});

// ── File matching ──────────────────────────────────────────────────────────────

describe('filename-to-subject matching', () => {
  it('matches files containing the animal_id_string to the correct subject', async () => {
    render(<FileDropZone rows={ROWS} date={DATE} />);
    dropFiles([
      makeFile(`${DATE}_2852_ephys_raw.bin`, 10_000_000_000),
      makeFile(`${DATE}_3076_trials.csv`, 10_000_000),
    ]);
    await waitFor(() => screen.getByTestId('drop-preview'));
    expect(screen.getByText(/Rat 2852/)).toBeInTheDocument();
    expect(screen.getByText(/Rat 3076/)).toBeInTheDocument();
    expect(screen.getByText(`${DATE}_2852_ephys_raw.bin`)).toBeInTheDocument();
    expect(screen.getByText(`${DATE}_3076_trials.csv`)).toBeInTheDocument();
  });

  it('shows unmatched section for files with no subject ID in name', async () => {
    render(<FileDropZone rows={ROWS} date={DATE} />);
    dropFiles([makeFile('unknown_file.csv', 1024)]);
    await waitFor(() => screen.getByTestId('drop-preview'));
    expect(screen.getByText(/unmatched/i)).toBeInTheDocument();
    expect(screen.getByText('unknown_file.csv')).toBeInTheDocument();
  });

  it('does not match a partial numeric overlap (2852 should not match 28520)', async () => {
    render(<FileDropZone rows={ROWS} date={DATE} />);
    dropFiles([makeFile('session_28520_data.csv', 1024)]);
    await waitFor(() => screen.getByTestId('drop-preview'));
    expect(screen.getByText(/unmatched/i)).toBeInTheDocument();
  });

  // ── animal_name matching ───────────────────────────────────────────────────

  it('matches by animal_name when the name appears literally in the filename', async () => {
    const animal = { id: 'b1', animal_name: 'Homer', animal_id_string: null };
    const row    = { animal, status: { id: 's-b1', animal_id: 'b1', date: `${DATE}T00:00:00.000Z` } };
    render(<FileDropZone rows={[row]} date={DATE} />);
    dropFiles([makeFile(`${DATE}_Homer_session.csv`)]);
    await waitFor(() => screen.getByTestId('drop-preview'));
    expect(screen.queryByText(/unmatched/i)).not.toBeInTheDocument();
    expect(screen.getByText(`${DATE}_Homer_session.csv`)).toBeInTheDocument();
  });

  it('matches multi-word animal_name with underscores as separator in filename', async () => {
    const animal = { id: 'b1', animal_name: 'Rat A', animal_id_string: null };
    const row    = { animal, status: { id: 's-b1', animal_id: 'b1', date: `${DATE}T00:00:00.000Z` } };
    render(<FileDropZone rows={[row]} date={DATE} />);
    dropFiles([makeFile(`${DATE}_Rat_A_trials.csv`)]);
    await waitFor(() => screen.getByTestId('drop-preview'));
    expect(screen.queryByText(/unmatched/i)).not.toBeInTheDocument();
  });

  it('matches multi-word animal_name with dashes as separator in filename', async () => {
    const animal = { id: 'b1', animal_name: 'Rat A', animal_id_string: null };
    const row    = { animal, status: { id: 's-b1', animal_id: 'b1', date: `${DATE}T00:00:00.000Z` } };
    render(<FileDropZone rows={[row]} date={DATE} />);
    dropFiles([makeFile(`${DATE}-Rat-A-session.bin`)]);
    await waitFor(() => screen.getByTestId('drop-preview'));
    expect(screen.queryByText(/unmatched/i)).not.toBeInTheDocument();
  });

  it('matches by animal_name even when animal_id_string is also set (OR logic)', async () => {
    const animal = { id: 'b1', animal_name: 'Rat A', animal_id_string: '2852' };
    const row    = { animal, status: { id: 's-b1', animal_id: 'b1', date: `${DATE}T00:00:00.000Z` } };
    render(<FileDropZone rows={[row]} date={DATE} />);
    // File uses name, not ID
    dropFiles([makeFile(`${DATE}_Rat_A_ephys.bin`)]);
    await waitFor(() => screen.getByTestId('drop-preview'));
    expect(screen.queryByText(/unmatched/i)).not.toBeInTheDocument();
  });

  it('does not match a partial name overlap (Rat should not match Rationale)', async () => {
    const animal = { id: 'b1', animal_name: 'Rat', animal_id_string: null };
    const row    = { animal, status: { id: 's-b1', animal_id: 'b1', date: `${DATE}T00:00:00.000Z` } };
    render(<FileDropZone rows={[row]} date={DATE} />);
    dropFiles([makeFile('Rationale_study_data.csv')]);
    await waitFor(() => screen.getByTestId('drop-preview'));
    expect(screen.getByText(/unmatched/i)).toBeInTheDocument();
  });

  it('shows warning for subjects with no matched files', async () => {
    render(<FileDropZone rows={ROWS} date={DATE} />);
    // Only drop file for 2852 — 3076 and 3077 get no match
    dropFiles([makeFile(`${DATE}_2852_trials.csv`, 1024)]);
    await waitFor(() => screen.getByTestId('drop-preview'));
    expect(screen.getByText(/no files matched/i)).toBeInTheDocument();
    expect(screen.getByText('3076')).toBeInTheDocument();
    expect(screen.getByText('3077')).toBeInTheDocument();
  });

  it('matches all four files per subject correctly', async () => {
    render(<FileDropZone rows={[{ animal: ANIMALS[0], status: STATUS_A1 }]} date={DATE} />);
    dropFiles([
      makeFile(`${DATE}_2852_ephys_raw.bin`,    10_000_000_000),
      makeFile(`${DATE}_2852_kilosort.h5`,       10_000_000_000),
      makeFile(`${DATE}_2852_trials.csv`,            10_000_000),
      makeFile(`${DATE}_2852_behavior_log.csv`,       10_000_000),
    ]);
    await waitFor(() => screen.getByTestId('drop-preview'));
    // Save button label is the authoritative "all 4 matched" signal
    expect(screen.getByTestId('save-btn')).toHaveTextContent('Register 4 files');
  });

  it('handles multiple subjects with 4 files each simultaneously', async () => {
    render(<FileDropZone rows={ROWS} date={DATE} />);
    dropFiles([
      makeFile(`${DATE}_2852_ephys_raw.bin`,    10_000_000_000),
      makeFile(`${DATE}_2852_kilosort.h5`,       10_000_000_000),
      makeFile(`${DATE}_2852_trials.csv`,            10_000_000),
      makeFile(`${DATE}_2852_behavior_log.csv`,       10_000_000),
      makeFile(`${DATE}_3076_ephys_raw.bin`,    10_000_000_000),
      makeFile(`${DATE}_3076_kilosort.h5`,       10_000_000_000),
      makeFile(`${DATE}_3076_trials.csv`,            10_000_000),
      makeFile(`${DATE}_3076_behavior_log.csv`,       10_000_000),
    ]);
    await waitFor(() => screen.getByTestId('drop-preview'));
    const saveBtn = screen.getByTestId('save-btn');
    expect(saveBtn.textContent).toMatch(/8 files/);
  });
});

// ── Remove before saving ───────────────────────────────────────────────────────

describe('remove files before saving', () => {
  it('removes a matched file when ✕ is clicked', async () => {
    render(<FileDropZone rows={ROWS} date={DATE} />);
    dropFiles([makeFile(`${DATE}_2852_trials.csv`, 1024)]);
    await waitFor(() => screen.getByText(`${DATE}_2852_trials.csv`));
    fireEvent.click(screen.getAllByText('✕')[0]);
    await waitFor(() =>
      expect(screen.queryByText(`${DATE}_2852_trials.csv`)).not.toBeInTheDocument()
    );
  });
});

// ── Saving ─────────────────────────────────────────────────────────────────────

describe('saving metadata', () => {
  it('calls sessionFilesApi.create with correct payload for each subject', async () => {
    const file = makeFile(`${DATE}_2852_trials.csv`); // size = 1 byte (content 'x')
    const savedFile = makeSavedFile('f1', 's-a1', file.name, String(file.size));
    sessionFilesApi.create.mockResolvedValue([savedFile]);

    render(<FileDropZone rows={ROWS} date={DATE} />);
    dropFiles([file]);
    await waitFor(() => screen.getByTestId('save-btn'));
    fireEvent.click(screen.getByTestId('save-btn'));

    await waitFor(() =>
      expect(sessionFilesApi.create).toHaveBeenCalledWith(
        's-a1',
        expect.arrayContaining([
          expect.objectContaining({
            filename:  `${DATE}_2852_trials.csv`,
            file_size: file.size,
            file_type: 'CSV',
          }),
        ])
      )
    );
  });

  it('does not call create for subjects with no matched files', async () => {
    sessionFilesApi.create.mockResolvedValue([]);
    render(<FileDropZone rows={ROWS} date={DATE} />);
    // Only 2852 gets a file
    dropFiles([makeFile(`${DATE}_2852_trials.csv`, 1024)]);
    await waitFor(() => screen.getByTestId('save-btn'));
    fireEvent.click(screen.getByTestId('save-btn'));

    await waitFor(() => expect(sessionFilesApi.create).toHaveBeenCalledTimes(1));
    expect(sessionFilesApi.create).toHaveBeenCalledWith('s-a1', expect.any(Array));
  });

  it('stores correct file_type classification from extension', async () => {
    sessionFilesApi.create.mockResolvedValue([]);
    render(<FileDropZone rows={[{ animal: ANIMALS[0], status: STATUS_A1 }]} date={DATE} />);
    dropFiles([
      makeFile(`${DATE}_2852_ephys.bin`,  10_000_000_000),
      makeFile(`${DATE}_2852_spikes.h5`,  10_000_000_000),
      makeFile(`${DATE}_2852_trials.csv`,     10_000_000),
      makeFile(`${DATE}_2852_log.csv`,        10_000_000),
    ]);
    await waitFor(() => screen.getByTestId('save-btn'));
    fireEvent.click(screen.getByTestId('save-btn'));

    await waitFor(() => expect(sessionFilesApi.create).toHaveBeenCalled());
    const [, files] = sessionFilesApi.create.mock.calls[0];
    expect(files.find((f) => f.filename.endsWith('.bin')).file_type).toBe('Binary');
    expect(files.find((f) => f.filename.endsWith('.h5')).file_type).toBe('HDF5');
    expect(files.filter((f) => f.filename.endsWith('.csv'))).toHaveLength(2);
    files.filter((f) => f.filename.endsWith('.csv')).forEach((f) => {
      expect(f.file_type).toBe('CSV');
    });
  });

  it('clears the drop preview after a successful save', async () => {
    sessionFilesApi.create.mockResolvedValue([makeSavedFile('f1', 's-a1', `${DATE}_2852_trials.csv`)]);
    render(<FileDropZone rows={ROWS} date={DATE} />);
    dropFiles([makeFile(`${DATE}_2852_trials.csv`, 1024)]);
    await waitFor(() => screen.getByTestId('save-btn'));
    fireEvent.click(screen.getByTestId('save-btn'));
    await waitFor(() =>
      expect(screen.queryByTestId('drop-preview')).not.toBeInTheDocument()
    );
  });

  it('shows an error message when create fails', async () => {
    sessionFilesApi.create.mockRejectedValue({ message: 'Server error' });
    render(<FileDropZone rows={ROWS} date={DATE} />);
    dropFiles([makeFile(`${DATE}_2852_trials.csv`, 1024)]);
    await waitFor(() => screen.getByTestId('save-btn'));
    fireEvent.click(screen.getByTestId('save-btn'));
    await waitFor(() => screen.getByText(/server error/i));
  });
});

// ── Deleting saved files ───────────────────────────────────────────────────────

describe('deleting saved files', () => {
  it('removes a saved file when its ✕ is clicked', async () => {
    sessionFilesApi.list.mockImplementation((id) =>
      id === 's-a1'
        ? Promise.resolve([makeSavedFile('f1', 's-a1', `${DATE}_2852_trials.csv`)])
        : Promise.resolve([])
    );
    render(<FileDropZone rows={ROWS} date={DATE} />);
    await waitFor(() => screen.getByText(`${DATE}_2852_trials.csv`));
    fireEvent.click(screen.getAllByText('✕')[0]);
    await waitFor(() =>
      expect(sessionFilesApi.delete).toHaveBeenCalledWith('f1')
    );
    await waitFor(() =>
      expect(screen.queryByText(`${DATE}_2852_trials.csv`)).not.toBeInTheDocument()
    );
  });
});
