// @vitest-environment jsdom
/**
 * Component tests for SpaceToolSettings (tool-policy / sensitive-tools UI).
 *
 * This is the PR #653 bug class: a sensitive *tool* (Bash) is delivered in a
 * separate `sensitiveTools` array, NOT in `categories`. The save patch
 * (buildPolicyPatch) must fold Bash's toggle into `enabledSensitive`, alongside
 * sensitive *categories* (ssh / browser). These tests render the real component
 * and assert that toggling Bash persists through to the PUT payload — the exact
 * regression that the earlier "categories-only" patch builder dropped.
 *
 * api + App.useAuthState are mocked so no real network and canManage=true.
 */
import '../../test/dom-setup';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithProviders } from '../../test/render-helpers';
import i18n from '../../i18n';

const { fetchSpaceToolPolicyMock, updateSpaceToolPolicyMock, fetchSpaceMembersMock } = vi.hoisted(() => ({
  fetchSpaceToolPolicyMock: vi.fn(),
  updateSpaceToolPolicyMock: vi.fn(),
  fetchSpaceMembersMock: vi.fn(),
}));

vi.mock('../../api', () => ({
  fetchSpaceToolPolicy: fetchSpaceToolPolicyMock,
  updateSpaceToolPolicy: updateSpaceToolPolicyMock,
  fetchSpaceMembers: fetchSpaceMembersMock,
}));

// auth.mode === 'disabled' => canManage = true (no owner check needed).
vi.mock('../../App', () => ({
  useAuthState: () => ({ mode: 'disabled' as const }),
}));

import { SpaceToolSettings } from './SpaceToolSettings';

const POLICY = {
  policy: { disabledSafe: [], enabledSensitive: [] },
  categories: [
    { name: 'web', sensitive: false, enabled: true },
    { name: 'office', sensitive: false, enabled: true },
    { name: 'ssh', sensitive: true, enabled: false },
    { name: 'browser', sensitive: true, enabled: false },
  ],
  // Bash arrives as a SEPARATE sensitive tool, not a category — the PR #653 trap.
  sensitiveTools: [{ name: 'Bash', enabled: false }],
};

beforeEach(() => {
  vi.clearAllMocks();
  // Labels like the Save button now route through i18next. Pin the language so
  // assertions on rendered text are deterministic regardless of detector state.
  void i18n.changeLanguage('ja');
  fetchSpaceMembersMock.mockResolvedValue([]);
  fetchSpaceToolPolicyMock.mockResolvedValue(structuredClone(POLICY));
  updateSpaceToolPolicyMock.mockResolvedValue(undefined);
});

async function renderLoaded() {
  renderWithProviders(<SpaceToolSettings spaceId="space-1" />);
  // Wait for the policy query to resolve and the Bash row to render.
  await waitFor(() => expect(screen.getByText('Bash')).toBeInTheDocument());
}

/** Find the role=switch toggle for a row identified by its label text. */
function switchFor(label: string): HTMLElement {
  // Walk up from the label until we reach an ancestor that also contains a switch.
  let el: HTMLElement | null = screen.getByText(label);
  while (el && el.parentElement) {
    el = el.parentElement;
    const sw = within(el).queryByRole('switch');
    if (sw) return sw;
  }
  throw new Error(`No switch found for row "${label}"`);
}

describe('SpaceToolSettings', () => {
  it('renders safe categories, sensitive categories, and the separate Bash tool', async () => {
    await renderLoaded();
    expect(screen.getByText('web')).toBeInTheDocument();
    expect(screen.getByText('ssh')).toBeInTheDocument();
    expect(screen.getByText('browser')).toBeInTheDocument();
    // Bash is the separately-delivered sensitive tool.
    expect(screen.getByText('Bash')).toBeInTheDocument();
  });

  it('Save is disabled until a toggle changes', async () => {
    await renderLoaded();
    const save = screen.getByRole('button', { name: '保存' });
    expect(save).toBeDisabled();
  });

  it('persists the Bash toggle into enabledSensitive (PR #653 regression)', async () => {
    await renderLoaded();
    // Each ToggleRow is a role="switch". Find Bash's switch by walking from its label.
    await userEvent.click(switchFor('Bash'));

    const save = screen.getByRole('button', { name: '保存' });
    expect(save).not.toBeDisabled();
    await userEvent.click(save);

    await waitFor(() => expect(updateSpaceToolPolicyMock).toHaveBeenCalledTimes(1));
    const [spaceId, patch] = updateSpaceToolPolicyMock.mock.calls[0];
    expect(spaceId).toBe('space-1');
    // The critical assertion: Bash must land in enabledSensitive, NOT be dropped.
    expect(patch.enabledSensitive).toContain('Bash');
    // No safe category was disabled.
    expect(patch.disabledSafe).toEqual([]);
  });

  it('persists a sensitive CATEGORY toggle (ssh) alongside the tool system', async () => {
    await renderLoaded();
    await userEvent.click(switchFor('ssh'));
    await userEvent.click(screen.getByRole('button', { name: '保存' }));

    await waitFor(() => expect(updateSpaceToolPolicyMock).toHaveBeenCalledTimes(1));
    const [, patch] = updateSpaceToolPolicyMock.mock.calls[0];
    expect(patch.enabledSensitive).toContain('ssh');
    expect(patch.enabledSensitive).not.toContain('Bash'); // untouched stays off
  });

  it('disabling a safe category lands in disabledSafe', async () => {
    await renderLoaded();
    await userEvent.click(switchFor('web')); // turn OFF (was on)
    await userEvent.click(screen.getByRole('button', { name: '保存' }));

    await waitFor(() => expect(updateSpaceToolPolicyMock).toHaveBeenCalledTimes(1));
    const [, patch] = updateSpaceToolPolicyMock.mock.calls[0];
    expect(patch.disabledSafe).toContain('web');
  });
});
