// @vitest-environment jsdom
/// <reference types="@vitest/browser/matchers" />
import '@testing-library/jest-dom/vitest';
import { WORKSPACE_STORAGE_TYPE } from '@learnitive/constants';
import { renderWithServices } from '@learnitive/test-utils';
import { WsPath } from '@learnitive/ws-path';
import { fireEvent, screen, waitFor, within } from '@testing-library/react';
import React, { act } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { PageWsHome } from '../page-ws-home';

async function setupWorkspaceWithNotes(noteNames: string[]) {
  const testRender = renderWithServices();
  const services = await testRender.autoMountServices();

  await act(async () => {
    await services.workspaceOps.createWorkspaceInfo({
      name: 'myWorkspace',
      type: WORKSPACE_STORAGE_TYPE.Memory,
      metadata: {},
    });

    for (const noteName of noteNames) {
      await services.fileSystem.createTextFile(
        `myWorkspace:${noteName}`,
        `# ${noteName}`,
      );
    }
  });

  act(() => {
    services.navigation.goWorkspace('myWorkspace');
  });

  await vi.waitFor(() => {
    expect(services.workspaceState.resolveAtoms().wsPaths).toHaveLength(
      noteNames.length,
    );
    expect(services.workspaceState.resolveAtoms().currentWsName).toBeDefined();
  });

  return { testRender, services };
}

function getRenderedNoteNames(): string[] {
  const table = screen.getByTestId('ws-home-project-files-table');
  // Note rows are the ones carrying a link; this skips the header and the
  // show-more sentinel row.
  return within(table)
    .getAllByRole('row')
    .slice(1)
    .flatMap((row) => {
      const link = within(row).queryByRole('link');
      return link?.textContent ? [link.textContent] : [];
    });
}

describe('PageWsHome', () => {
  afterEach(() => {
    vi.restoreAllMocks();
  });

  it('shows empty state if no notes', async () => {
    const testRender = renderWithServices();
    const services = await testRender.autoMountServices();
    await act(async () => {
      await services.workspaceOps.createWorkspaceInfo({
        name: 'myWorkspace',
        type: WORKSPACE_STORAGE_TYPE.Memory,
        metadata: {},
      });
    });

    testRender.mountComponent({ ui: <PageWsHome /> });

    act(() => {
      services.navigation.goWorkspace('myWorkspace');
    });

    await vi.waitFor(() => {
      expect(
        services.workspaceState.resolveAtoms().currentWsName,
      ).toBeDefined();
    });

    expect(screen.getByText(/no project files found/i)).toBeInTheDocument();
  });

  it('lists every note ordered by modification recency', async () => {
    // Give each Date.now call a strictly increasing value so notes created
    // later always carry a larger mtime.
    let now = 1_000_000;
    vi.spyOn(Date, 'now').mockImplementation(() => {
      now += 1000;
      return now;
    });

    const { testRender } = await setupWorkspaceWithNotes([
      'alpha.md',
      'zeta.md',
      'beta.md',
    ]);

    testRender.mountComponent({ ui: <PageWsHome /> });

    await waitFor(() => {
      expect(getRenderedNoteNames()).toEqual(['beta.md', 'zeta.md', 'alpha.md']);
    });
  });

  it('sorts by name when the name header is toggled', async () => {
    const { testRender } = await setupWorkspaceWithNotes([
      'zeta.md',
      'alpha.md',
      'beta.md',
    ]);

    testRender.mountComponent({ ui: <PageWsHome /> });

    await waitFor(() => {
      expect(getRenderedNoteNames()).toHaveLength(3);
    });

    fireEvent.click(screen.getByRole('button', { name: /sort by name/i }));
    await waitFor(() => {
      expect(getRenderedNoteNames()).toEqual(['alpha.md', 'beta.md', 'zeta.md']);
    });

    fireEvent.click(screen.getByRole('button', { name: /sort by name/i }));
    await waitFor(() => {
      expect(getRenderedNoteNames()).toEqual(['zeta.md', 'beta.md', 'alpha.md']);
    });
  });

  it('filters notes by name and folder, and reports empty matches', async () => {
    const { testRender } = await setupWorkspaceWithNotes([
      'groceries.md',
      'work/meeting.md',
      'work/roadmap.md',
    ]);

    testRender.mountComponent({ ui: <PageWsHome /> });

    await waitFor(() => {
      expect(getRenderedNoteNames()).toHaveLength(3);
    });

    const filterInput = screen.getByRole('textbox', {
      name: /filter project files/i,
    });

    fireEvent.change(filterInput, { target: { value: 'meet' } });
    await waitFor(() => {
      expect(getRenderedNoteNames()).toEqual(['meeting.md']);
    });

    // Folder names match too, so a folder query reveals its notes.
    fireEvent.change(filterInput, { target: { value: 'work' } });
    await waitFor(() => {
      expect(getRenderedNoteNames().sort()).toEqual(['meeting.md', 'roadmap.md']);
    });

    fireEvent.change(filterInput, { target: { value: 'nothing-matches' } });
    await waitFor(() => {
      expect(
        screen.getByText(/no project files match your filter/i),
      ).toBeInTheDocument();
    });
  });

  it('shows folder location and star indicator', async () => {
    const { testRender, services } = await setupWorkspaceWithNotes([
      'root-note.md',
      'work/starred-note.md',
    ]);

    await act(async () => {
      await services.userActivityService.toggleStarItem(
        WsPath.fromString('myWorkspace:work/starred-note.md'),
        true,
      );
    });

    testRender.mountComponent({ ui: <PageWsHome /> });

    await waitFor(() => {
      expect(getRenderedNoteNames()).toHaveLength(2);
    });

    // The starred atom resolves asynchronously, so the indicator can lag the
    // row itself.
    await waitFor(() => {
      const table = screen.getByTestId('ws-home-project-files-table');
      const starredRow = within(table)
        .getAllByRole('row')
        .find((row) => within(row).queryByText('starred-note.md'));
      expect(starredRow).toBeDefined();
      expect(
        within(starredRow as HTMLElement).getByText('Starred'),
      ).toBeInTheDocument();
      expect(within(starredRow as HTMLElement).getByText('work')).toBeVisible();
    });
  });

  it('respects persisted column visibility from workbench state', async () => {
    const { testRender, services } = await setupWorkspaceWithNotes([
      'note1.md',
    ]);

    act(() => {
      testRender.testEnv.commonOpts.store.set(
        services.workbenchState.$notesTableColumnVisibility,
        { location: false, lastOpenedAt: true },
      );
    });

    testRender.mountComponent({ ui: <PageWsHome /> });

    await waitFor(() => {
      expect(getRenderedNoteNames()).toHaveLength(1);
    });

    expect(
      screen.queryByRole('button', { name: /sort by location/i }),
    ).not.toBeInTheDocument();
    expect(
      screen.getByRole('button', { name: /sort by last opened/i }),
    ).toBeInTheDocument();
  });

  // This deliberately renders the full 150-row chunk and is slower on
  // constrained CI workers than the surrounding small-workspace cases.
  it('renders large workspaces in chunks behind a show-more control', async () => {
    const noteNames = Array.from(
      { length: 160 },
      (_, index) => `note-${String(index).padStart(3, '0')}.md`,
    );
    const { testRender } = await setupWorkspaceWithNotes(noteNames);

    testRender.mountComponent({ ui: <PageWsHome /> });

    await waitFor(() => {
      expect(getRenderedNoteNames()).toHaveLength(150);
    });

    const showMore = screen.getByRole('button', { name: /show 10 more/i });
    fireEvent.click(showMore);

    await waitFor(() => {
      expect(getRenderedNoteNames()).toHaveLength(160);
    });
    expect(
      screen.queryByRole('button', { name: /show .* more/i }),
    ).not.toBeInTheDocument();
  }, 20_000);

  it('keeps a note row rendered when its file stat read fails', async () => {
    const { testRender, services } = await setupWorkspaceWithNotes([
      'healthy.md',
      'broken.md',
    ]);

    const originalFileStat = services.fileSystem.fileStat.bind(
      services.fileSystem,
    );
    vi.spyOn(services.fileSystem, 'fileStat').mockImplementation(
      async (wsPath, options) => {
        if (wsPath === 'myWorkspace:broken.md') {
          throw new Error('stat failed');
        }
        return originalFileStat(wsPath, options);
      },
    );

    testRender.mountComponent({ ui: <PageWsHome /> });

    await waitFor(() => {
      expect(getRenderedNoteNames().sort()).toEqual(['broken.md', 'healthy.md']);
    });

    const table = screen.getByTestId('ws-home-project-files-table');
    const brokenRow = within(table)
      .getAllByRole('row')
      .find((row) => within(row).queryByText('broken.md'));
    // Visible columns are name, location, modified, actions: the modified
    // cell of the failed row stays an em-dash placeholder.
    const brokenCells = within(brokenRow as HTMLElement).getAllByRole('cell');
    expect(brokenCells[2]).toHaveTextContent('—');

    const healthyRow = within(table)
      .getAllByRole('row')
      .find((row) => within(row).queryByText('healthy.md'));
    const healthyCells = within(healthyRow as HTMLElement).getAllByRole('cell');
    expect(healthyCells[2]).not.toHaveTextContent('—');
  });
});
