// @vitest-environment happy-dom
import '@testing-library/jest-dom/vitest';
import { render, screen, fireEvent, act } from '@testing-library/react';
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { ReviewSidebar } from './ReviewSidebar';
import type { FileDiff, ReviewComment, CommitInfo } from './types';

// Mock ResizeObserver — not provided by happy-dom.
// react-window 2.x uses `new ResizeObserver()` internally.
class MockResizeObserver {
  observe() {}
  unobserve() {}
  disconnect() {}
}
vi.stubGlobal('ResizeObserver', MockResizeObserver);

// Mock react-window to avoid ESM/CJS duplicate-React-instance errors in Vitest.
// The mock renders only the first 15 rows, simulating windowed rendering.
// It also exposes a `listRef`-compatible imperative handle so scroll-save/restore logic runs.
vi.mock('react-window', () => ({
  List: vi.fn(({ rowComponent: RowComponent, rowCount, rowProps, listRef }: any) => {
    // Expose the imperative API via the ref (synchronous assignment is safe in tests).
    if (listRef && typeof listRef === 'object' && 'current' in listRef) {
      listRef.current = { element: { scrollTop: 0 }, scrollToRow: vi.fn() };
    }
    const renderCount = Math.min(rowCount ?? 0, 15);
    return (
      <div data-testid="virtual-list">
        {Array.from({ length: renderCount }, (_, i) => (
          <RowComponent key={i} index={i} style={{}} ariaAttributes={{}} {...rowProps} />
        ))}
      </div>
    );
  }),
}));

// ─── Helpers ──────────────────────────────────────────────────────────────────

function makeFile(path: string): FileDiff {
  return {
    oldPath: path,
    newPath: path,
    hunks: [],
    additions: 1,
    deletions: 0,
    changeType: 'modified',
  };
}

function makeFiles(count: number, prefix = 'src/components/'): FileDiff[] {
  return Array.from({ length: count }, (_, i) =>
    makeFile(`${prefix}file${String(i).padStart(4, '0')}.ts`),
  );
}

const NO_COMMENTS: ReviewComment[] = [];
const NO_COMMITS: CommitInfo[] = [];

function renderSidebar(files: FileDiff[]) {
  return render(
    <ReviewSidebar
      files={files}
      comments={NO_COMMENTS}
      onFileClick={vi.fn()}
      selectedCommit={null}
      activeFiles={files}
      commits={NO_COMMITS}
      onSelectCommit={vi.fn()}
    />,
  );
}

// ─── Tests ────────────────────────────────────────────────────────────────────

describe('ReviewSidebar FilesView virtualization', () => {
  beforeEach(() => vi.clearAllMocks());
  afterEach(() => vi.restoreAllMocks());

  // 1. Virtual list NOT used for ≤50 files (fallback path)
  it('does not use virtual list when files count is ≤50', () => {
    renderSidebar(makeFiles(10));

    expect(screen.queryByTestId('virtual-list')).not.toBeInTheDocument();
    // All 10 file rows are in the DOM directly
    expect(document.querySelectorAll('[data-testid="file-row"]').length).toBe(10);
  });

  // 2. Virtual list IS used for >50 files (virtualized path)
  it('uses virtual list when files count is >50', () => {
    renderSidebar(makeFiles(1000));

    expect(screen.getByTestId('virtual-list')).toBeInTheDocument();
    // Mock renders only 15 rows — far fewer than 1000
    expect(document.querySelectorAll('[data-testid="file-row"]').length).toBeLessThan(50);
  });

  // 3. Directory collapse removes file rows from the virtual list
  it('removes file rows from virtual list when directory is collapsed', async () => {
    // 100 files all in "src/" — produces 101 rows (1 dir-header + 100 files), which is >50
    const files = Array.from({ length: 100 }, (_, i) => makeFile(`src/file${i}.ts`));
    renderSidebar(files);

    expect(screen.getByTestId('virtual-list')).toBeInTheDocument();

    const dirHeader = screen.getByRole('button', { name: /src\// });
    expect(document.querySelectorAll('[data-testid="file-row"]').length).toBeGreaterThan(0);

    await act(async () => {
      fireEvent.click(dirHeader);
    });

    // After collapse: only the dir-header row remains in the virtual list
    expect(document.querySelectorAll('[data-testid="file-row"]').length).toBe(0);
    expect(document.querySelectorAll('[data-testid="dir-header"]').length).toBe(1);
  });

  // 3a. Expanding a collapsed directory restores file rows
  it('restores file rows when a collapsed directory is expanded again', async () => {
    const files = makeFiles(60, 'src/components/');
    renderSidebar(files);

    const dirHeader = screen.getByRole('button', { name: /src\/components\// });

    // Collapse
    await act(async () => {
      fireEvent.click(dirHeader);
    });
    expect(document.querySelectorAll('[data-testid="file-row"]').length).toBe(0);

    // Expand again
    const freshDirHeader = screen.getByRole('button', { name: /src\/components\// });
    await act(async () => {
      fireEvent.click(freshDirHeader);
    });

    // File rows are back (virtual list renders up to 15)
    expect(document.querySelectorAll('[data-testid="file-row"]').length).toBeGreaterThan(0);
    expect(document.querySelectorAll('[data-testid="dir-header"]').length).toBeGreaterThan(0);
  });

  // 4. Scroll position saved and restored on Files ↔ Commits tab switch
  it('restores file rows when returning to Files tab after switching to Commits tab', async () => {
    renderSidebar(makeFiles(200));

    // Files tab is default — file rows are visible
    expect(document.querySelectorAll('[data-testid="file-row"]').length).toBeGreaterThan(0);

    // Switch to Commits tab — FilesView unmounts (scroll offset is saved)
    await act(async () => {
      fireEvent.click(screen.getByTitle('Commits'));
    });
    expect(document.querySelectorAll('[data-testid="file-row"]').length).toBe(0);

    // Switch back to Files tab — FilesView remounts (scroll offset is restored)
    await act(async () => {
      fireEvent.click(screen.getByTitle('Files'));
    });
    expect(document.querySelectorAll('[data-testid="file-row"]').length).toBeGreaterThan(0);
  });

  // 5. Clicking a file calls onFileClick with the correct path
  it('calls onFileClick when a file row is clicked', () => {
    const onFileClick = vi.fn();
    const files = makeFiles(5);
    render(
      <ReviewSidebar
        files={files}
        comments={NO_COMMENTS}
        onFileClick={onFileClick}
        selectedCommit={null}
        activeFiles={files}
        commits={NO_COMMITS}
        onSelectCommit={vi.fn()}
      />,
    );

    const fileButtons = document.querySelectorAll('[data-testid="file-row"]');
    expect(fileButtons.length).toBeGreaterThan(0);
    fireEvent.click(fileButtons[0]);

    // First file after alphabetical sort within the directory
    expect(onFileClick).toHaveBeenCalledWith(files[0].newPath);
  });

  // 6. Root-level files (no subdirectory) render without a directory header
  it('root-level files render without a directory header', () => {
    const files = makeFiles(10, ''); // no prefix → root-level files
    renderSidebar(files);

    expect(document.querySelectorAll('[data-testid="file-row"]').length).toBe(10);
    expect(document.querySelectorAll('[data-testid="dir-header"]').length).toBe(0);
  });
});
