// @vitest-environment jsdom
/**
 * Component test for SpaceRail — the workspace switcher rail. Mocks the data
 * hooks (useSpaces, useLocalTaskList) and useAuthState so the real grouping,
 * "自分" badge, running-count badge, selection, empty/loading/error states and
 * the create dialog toggle are exercised. SpaceFormDialog is stubbed so opening
 * it doesn't pull in the full create-dialog tree.
 */
import '../../test/dom-setup';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import type { Space } from '../../api';
import i18n from '../../i18n';

// --- Mocks (declared before importing the component under test) -------------
const useSpacesMock = vi.fn();
const useTaskListMock = vi.fn();
const useAuthStateMock = vi.fn();
const updateDisplayMock = { mutateAsync: vi.fn() };

vi.mock('../../hooks/useSpaces', () => ({
  useSpaces: () => useSpacesMock(),
  useUpdateSpaceDisplayPrefs: () => updateDisplayMock,
}));
vi.mock('../../hooks/useTaskList', () => ({ useLocalTaskList: () => useTaskListMock() }));
vi.mock('../../App', () => ({ useAuthState: () => useAuthStateMock() }));
vi.mock('./SpaceFormDialog', () => ({
  SpaceFormDialog: ({ onSaved }: { onSaved: (id: string) => void }) => (
    <div data-testid="space-form-dialog">
      <button onClick={() => onSaved('new-space')}>save</button>
    </div>
  ),
}));

import { SpaceRail } from './SpaceRail';

function space(p: Partial<Space> & Pick<Space, 'id' | 'kind' | 'title'>): Space {
  return {
    description: '',
    ownerId: null,
    visibility: 'private',
    visibilityScopeOrgId: null,
    status: 'open',
    brandColor: null,
    workspaceDir: null,
    createdAt: '2026-01-01T00:00:00Z',
    updatedAt: '2026-01-01T00:00:00Z',
    ...p,
  } as Space;
}

beforeEach(() => {
  // Rail labels (loading/error/empty/running-count/"自分" badge) now route through
  // i18next. Pin the language so text assertions are deterministic.
  void i18n.changeLanguage('ja');
  useSpacesMock.mockReset();
  useTaskListMock.mockReset();
  useAuthStateMock.mockReset();
  updateDisplayMock.mutateAsync.mockReset();
  updateDisplayMock.mutateAsync.mockResolvedValue({ favorite: false, hidden: false });
  useTaskListMock.mockReturnValue({ data: [] });
  useAuthStateMock.mockReturnValue({ mode: 'disabled' });
});

describe('SpaceRail', () => {
  it('shows the loading state', () => {
    useSpacesMock.mockReturnValue({ data: undefined, isLoading: true, isError: false });
    render(<SpaceRail onSelect={() => {}} />);
    expect(screen.getByText('読み込み中…')).toBeInTheDocument();
  });

  it('shows the error state', () => {
    useSpacesMock.mockReturnValue({ data: undefined, isLoading: false, isError: true });
    render(<SpaceRail onSelect={() => {}} />);
    expect(screen.getByText('ワークスペースを取得できませんでした')).toBeInTheDocument();
  });

  it('shows the empty state when there are no spaces', () => {
    useSpacesMock.mockReturnValue({ data: [], isLoading: false, isError: false });
    render(<SpaceRail onSelect={() => {}} />);
    expect(
      screen.getByText('ワークスペースがありません。「＋ 新規」から作成してください。'),
    ).toBeInTheDocument();
  });

  it('groups personal and case spaces under their headers', () => {
    useSpacesMock.mockReturnValue({
      data: [
        space({ id: 'p1', kind: 'personal', title: 'My Workspace' }),
        space({ id: 'c1', kind: 'case', title: 'Project A' }),
      ],
      isLoading: false,
      isError: false,
    });
    render(<SpaceRail onSelect={() => {}} />);
    const groups = screen.getAllByTestId('space-group');
    expect(groups.map((g) => g.getAttribute('data-group'))).toEqual([
      '統合スペース',
      '個別スペース',
    ]);
    expect(screen.getByText('My Workspace')).toBeInTheDocument();
    expect(screen.getByText('Project A')).toBeInTheDocument();
  });

  it('shows favorites in a dedicated section and hides hidden workspaces from the normal list', () => {
    useSpacesMock.mockReturnValue({
      data: [
        space({ id: 'fav', kind: 'case', title: 'Favorite Project', favorite: true }),
        space({ id: 'normal', kind: 'case', title: 'Normal Project' }),
        space({ id: 'hidden', kind: 'case', title: 'Hidden Project', hidden: true }),
      ],
      isLoading: false,
      isError: false,
    });
    render(<SpaceRail onSelect={() => {}} />);
    const groups = screen.getAllByTestId('space-group');
    expect(groups.map((g) => g.getAttribute('data-group'))).toContain('お気に入り');
    expect(screen.getByText('Favorite Project')).toBeInTheDocument();
    expect(screen.getByText('Normal Project')).toBeInTheDocument();
    expect(screen.queryByText('Hidden Project')).toBeNull();

    fireEvent.click(screen.getByTestId('space-hidden-toggle'));
    expect(screen.getByText('Hidden Project')).toBeInTheDocument();
    expect(screen.getByTestId('space-hidden-badge')).toHaveTextContent('非表示');
  });

  it('search includes hidden workspaces with a hidden badge', () => {
    useSpacesMock.mockReturnValue({
      data: [
        space({ id: 'visible', kind: 'case', title: 'Visible Project' }),
        space({ id: 'hidden', kind: 'case', title: 'Hidden Project', hidden: true }),
      ],
      isLoading: false,
      isError: false,
    });
    render(<SpaceRail onSelect={() => {}} />);
    fireEvent.change(screen.getByTestId('space-search-input'), { target: { value: 'hidden' } });
    expect(screen.getByText('Hidden Project')).toBeInTheDocument();
    expect(screen.getByTestId('space-hidden-badge')).toHaveTextContent('非表示');
  });

  it('calls onSelect with the space id when a row is clicked', () => {
    const onSelect = vi.fn();
    useSpacesMock.mockReturnValue({
      data: [space({ id: 'c1', kind: 'case', title: 'Project A' })],
      isLoading: false,
      isError: false,
    });
    render(<SpaceRail onSelect={onSelect} />);
    fireEvent.click(screen.getByText('Project A'));
    expect(onSelect).toHaveBeenCalledWith('c1');
  });

  it('renders a running-count badge when a space has running tasks', () => {
    useSpacesMock.mockReturnValue({
      data: [space({ id: 'c1', kind: 'case', title: 'Project A' })],
      isLoading: false,
      isError: false,
    });
    useTaskListMock.mockReturnValue({
      data: [
        { spaceId: 'c1', latestJob: { status: 'running' } },
        { spaceId: 'c1', latestJob: { status: 'succeeded' } },
      ],
    });
    render(<SpaceRail onSelect={() => {}} />);
    const badge = screen.getByTestId('space-running-count');
    expect(badge).toHaveTextContent('1 実行中');
  });

  it('shows the "自分" badge only when other users\' spaces are present (admin view)', () => {
    useAuthStateMock.mockReturnValue({ mode: 'authenticated', user: { id: 'me' } });
    useSpacesMock.mockReturnValue({
      data: [
        space({ id: 'c1', kind: 'case', title: 'Mine', ownerId: 'me' }),
        space({ id: 'c2', kind: 'case', title: 'Theirs', ownerId: 'other' }),
      ],
      isLoading: false,
      isError: false,
    });
    render(<SpaceRail onSelect={() => {}} />);
    const badges = screen.getAllByTestId('space-mine-badge');
    expect(badges).toHaveLength(1);
    // Badge lives inside the owner's row.
    const mineRow = screen.getByText('Mine').closest('[data-testid="space-row"]');
    expect(mineRow).toHaveAttribute('data-space-mine', '1');
  });

  it('hides the "自分" badge when all spaces belong to the viewer', () => {
    useAuthStateMock.mockReturnValue({ mode: 'authenticated', user: { id: 'me' } });
    useSpacesMock.mockReturnValue({
      data: [space({ id: 'c1', kind: 'case', title: 'Mine', ownerId: 'me' })],
      isLoading: false,
      isError: false,
    });
    render(<SpaceRail onSelect={() => {}} />);
    expect(screen.queryByTestId('space-mine-badge')).toBeNull();
  });

  it('opens the create dialog and selects the new space on save', () => {
    const onSelect = vi.fn();
    useSpacesMock.mockReturnValue({ data: [], isLoading: false, isError: false });
    render(<SpaceRail onSelect={onSelect} />);
    expect(screen.queryByTestId('space-form-dialog')).toBeNull();
    fireEvent.click(screen.getByTestId('create-space-btn'));
    expect(screen.getByTestId('space-form-dialog')).toBeInTheDocument();
    fireEvent.click(screen.getByText('save'));
    expect(onSelect).toHaveBeenCalledWith('new-space');
    expect(screen.queryByTestId('space-form-dialog')).toBeNull();
  });

  it('updates display settings from the row menu and can undo the action', async () => {
    useSpacesMock.mockReturnValue({
      data: [space({ id: 'c1', kind: 'case', title: 'Project A' })],
      isLoading: false,
      isError: false,
    });
    render(<SpaceRail onSelect={() => {}} />);

    fireEvent.click(screen.getByTestId('space-row-menu'));
    fireEvent.click(screen.getByText('非表示にする'));
    await waitFor(() => expect(updateDisplayMock.mutateAsync).toHaveBeenCalledWith({ id: 'c1', patch: { hidden: true } }));
    expect(screen.getByTestId('space-display-toast')).toHaveTextContent('「Project A」を非表示にしました');

    fireEvent.click(screen.getByTestId('space-display-undo'));
    await waitFor(() => expect(updateDisplayMock.mutateAsync).toHaveBeenCalledWith({ id: 'c1', patch: { favorite: false, hidden: false } }));
  });
});
