// @vitest-environment happy-dom
//
// A revision that touched exactly ONE file has only one thing you could act
// on, so the modal preselects it — that turns "Show selected in graph view"
// and "Revert selected" into one click instead of two. Multi-file revisions
// deliberately start with nothing selected: there is no basis for guessing
// which file the user meant.
//
// Also covers windowing: revisions in the field carry 1,267–1,500 changes,
// and a full synchronous render (~7k DOM nodes) is the measured "double-click
// a revision takes a second" stall (LatencyIssueGuesses §9.4).
//
// ServerDataContext is mocked for getUser (same style as
// ConnectionsModal.test.jsx); UserAvatar and ModalPortal are stubbed so
// content renders in place (still inside document.body via the mount
// container) instead of behind a real portal/avatar-resolution round trip.
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('../../context/ServerDataContext', () => ({
  useServerData: () => ({
    getUser: () => ({ displayName: 'Alice', username: '@alice', avatar: null, avatarUrl: null }),
  }),
}));
vi.mock('../UserAvatar', () => ({
  UserAvatar: () => <span data-testid="avatar" />,
}));
vi.mock('./ModalPortal', () => ({
  ModalPortal: ({ children }) => children,
}));

import { RevisionDetailModal } from './RevisionDetailModal';

let container;
let currentRoot;

beforeEach(() => {
  container = document.createElement('div');
  document.body.appendChild(container);
});

afterEach(async () => {
  // The modal portals into document.body in production, so an explicit
  // unmount (rather than just dropping the container) is what actually
  // clears it between tests.
  if (currentRoot) await act(async () => currentRoot.unmount());
  currentRoot = null;
  document.body.removeChild(container);
  container = null;
});

function entryWith(paths) {
  return {
    revision: 7,
    date: '2026-07-27 10:00',
    author: '@alice',
    description: 'a change',
    changes: paths.map((path) => ({ path, change: 'edited' })),
  };
}

function render(props) {
  const root = createRoot(container);
  currentRoot = root;
  act(() =>
    root.render(
      <RevisionDetailModal
        entry={entryWith(['Source/main.cpp'])}
        onClose={() => {}}
        onShowInGraphView={() => {}}
        onRollback={() => {}}
        initialSelectedPaths={null}
        {...props}
      />
    )
  );
  return root;
}

const selectedRows = () => [
  ...document.body.querySelectorAll('.revision-file-row--selected .revision-file-path'),
].map((el) => el.textContent);

const graphButton = () =>
  [...document.body.querySelectorAll('.revision-pill-btn')].find(
    (b) => b.textContent === 'Show selected in graph view'
  );

const revertButton = () => document.body.querySelector('.revision-pill-btn--danger');

describe('RevisionDetailModal preselection', () => {
  it('preselects the only file of a single-file revision', () => {
    render();
    expect(selectedRows()).toEqual(['Source/main.cpp']);
  });

  it('leaves Show selected in graph view immediately usable for one file', () => {
    render();
    expect(graphButton().disabled).toBe(false);
    expect(revertButton().textContent).toBe('Revert selected files back to this revision');
  });

  it('selects nothing when the revision touched more than one file', () => {
    render({ entry: entryWith(['Source/main.cpp', 'Source/util.cpp']) });
    expect(selectedRows()).toEqual([]);
    expect(graphButton().disabled).toBe(true);
    expect(revertButton().textContent).toBe('Revert all files back to this revision');
  });

  it('still honours an explicit preselection from a graph card', () => {
    render({
      entry: entryWith(['Source/main.cpp', 'Source/util.cpp']),
      initialSelectedPaths: ['Source/util.cpp'],
    });
    expect(selectedRows()).toEqual(['Source/util.cpp']);
  });

  it('does not override an explicit preselection on a single-file revision', () => {
    // Defensive: a graph card for the one changed file must not end up with a
    // duplicate or a different selection than it asked for.
    render({ initialSelectedPaths: ['Source/main.cpp'] });
    expect(selectedRows()).toEqual(['Source/main.cpp']);
  });
});

describe('RevisionDetailModal windowing', () => {
  const bigEntry = {
    revision: 7,
    author: '@alice',
    date: '2026-07-27',
    description: 'stress batch',
    changes: Array.from({ length: 1000 }, (_, i) => ({
      path: `assets/file-${String(i).padStart(4, '0')}.bin`,
      change: 'edit',
    })),
  };

  it('renders a window of rows, not all 1000', async () => {
    const root = createRoot(container);
    currentRoot = root;
    await act(async () => {
      root.render(<RevisionDetailModal entry={bigEntry} onClose={() => {}} />);
    });
    const rows = container.querySelectorAll('.revision-file-row');
    expect(rows.length).toBeGreaterThan(0);
    expect(rows.length).toBeLessThan(100);
    // Window starts at the top of the list.
    expect(rows[0].textContent).toContain('assets/file-0000.bin');
    // Off-window rows are represented by spacer padding so the scrollbar
    // geometry matches a full render.
    const list = container.querySelector('.revision-file-list');
    expect(parseInt(list.style.paddingBottom, 10)).toBeGreaterThan(0);
  });

  it('still shows the true file count in the meta row', async () => {
    const root = createRoot(container);
    currentRoot = root;
    await act(async () => {
      root.render(<RevisionDetailModal entry={bigEntry} onClose={() => {}} />);
    });
    expect(container.textContent).toContain('1000');
  });
});
