import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, describe, expect, it, vi } from 'vitest';

vi.mock('@renderer/api', () => ({
  api: {
    teams: {
      updateConfig: vi.fn(async () => {}),
      replaceMembers: vi.fn(async () => {}),
      removeMember: vi.fn(async () => {}),
      restartMember: vi.fn(async () => {}),
    },
  },
}));

vi.mock('@renderer/components/team/members/MembersEditorSection', () => ({
  MembersEditorSection: ({
    members,
    onChange,
    fieldError,
    headerExtra,
  }: {
    members: Array<{
      id: string;
      name: string;
      originalName?: string;
      roleSelection?: string;
      customRole?: string;
      providerId?: string;
      model?: string;
      effort?: string;
      mcpPolicy?: { mode: 'appOnly' };
    }>;
    onChange: (
      members: Array<{
        id: string;
        name: string;
        originalName?: string;
        roleSelection?: string;
        customRole?: string;
        providerId?: string;
        model?: string;
        effort?: string;
        mcpPolicy?: { mode: 'appOnly' };
      }>
    ) => void;
    fieldError?: string;
    headerExtra?: React.ReactNode;
  }) =>
    React.createElement(
      'div',
      null,
      'members-editor',
      headerExtra,
      React.createElement(
        'button',
        {
          type: 'button',
          'data-testid': 'rename-existing-member',
          onClick: () =>
            onChange(
              members.map((member, index) =>
                index === 0 ? { ...member, name: 'alice-renamed' } : member
              )
            ),
        },
        'rename-existing-member'
      ),
      React.createElement(
        'button',
        {
          type: 'button',
          'data-testid': 'remove-existing-member',
          onClick: () => onChange(members.slice(1)),
        },
        'remove-existing-member'
      ),
      React.createElement(
        'button',
        {
          type: 'button',
          'data-testid': 'duplicate-member-name',
          onClick: () =>
            onChange(
              members.map((member, index) =>
                index === 1 ? { ...member, name: members[0]?.name ?? member.name } : member
              )
            ),
        },
        'duplicate-member-name'
      ),
      React.createElement(
        'button',
        {
          type: 'button',
          'data-testid': 'invalid-member-name',
          onClick: () =>
            onChange(
              members.map((member, index) =>
                index === 0 ? { ...member, name: 'team-lead' } : member
              )
            ),
        },
        'invalid-member-name'
      ),
      React.createElement(
        'button',
        {
          type: 'button',
          'data-testid': 'change-member-runtime',
          onClick: () =>
            onChange(
              members.map((member, index) =>
                index === 0 ? { ...member, providerId: 'codex', model: 'gpt-5.4' } : member
              )
            ),
        },
        'change-member-runtime'
      ),
      React.createElement(
        'button',
        {
          type: 'button',
          'data-testid': 'change-member-mcp-policy',
          onClick: () =>
            onChange(
              members.map((member, index) =>
                index === 0 ? { ...member, mcpPolicy: { mode: 'appOnly' } } : member
              )
            ),
        },
        'change-member-mcp-policy'
      ),
      React.createElement(
        'button',
        {
          type: 'button',
          'data-testid': 'revert-member-runtime',
          onClick: () =>
            onChange(
              members.map((member, index) =>
                index === 0 ? { ...member, providerId: 'codex', model: 'gpt-5.2' } : member
              )
            ),
        },
        'revert-member-runtime'
      ),
      React.createElement(
        'button',
        {
          type: 'button',
          'data-testid': 'change-member-role',
          onClick: () =>
            onChange(
              members.map((member, index) =>
                index === 0 ? { ...member, roleSelection: 'developer' } : member
              )
            ),
        },
        'change-member-role'
      ),
      React.createElement(
        'button',
        {
          type: 'button',
          'data-testid': 'add-new-member',
          onClick: () =>
            onChange([
              ...members,
              {
                id: 'draft-new',
                name: 'charlie',
                roleSelection: '',
                customRole: '',
              },
            ]),
        },
        'add-new-member'
      ),
      React.createElement(
        'button',
        {
          type: 'button',
          'data-testid': 'remove-new-member',
          onClick: () => onChange(members.filter((member) => member.id !== 'draft-new')),
        },
        'remove-new-member'
      ),
      fieldError ? React.createElement('div', { 'data-testid': 'members-field-error' }, fieldError) : null
    ),
  buildMembersFromDrafts: vi.fn((members) =>
    (
      members as Array<{
        name: string;
        roleSelection?: string;
        customRole?: string;
        providerId?: string;
        model?: string;
        effort?: string;
        mcpPolicy?: { mode: 'appOnly' };
      }>
    ).map((member) => ({
      name: member.name,
      role:
        member.roleSelection === 'developer'
          ? 'Developer'
          : member.roleSelection === 'reviewer'
            ? 'Reviewer'
            : member.customRole || undefined,
      providerId: member.providerId,
      model: member.model,
      effort: member.effort,
      mcpPolicy: member.mcpPolicy,
    }))
  ),
  createMemberDraftsFromInputs: vi.fn((members) =>
    (
      members as Array<{
        name: string;
        role?: string;
        providerId?: string;
        model?: string;
        effort?: string;
        mcpPolicy?: { mode: 'appOnly' };
      }>
    ).map((member, index) => ({
      id: `draft-${index}`,
      name: member.name,
      originalName: member.name,
      roleSelection:
        member.role === 'Developer'
          ? 'developer'
          : member.role === 'Reviewer'
            ? 'reviewer'
            : member.role
              ? '__custom__'
              : '',
      customRole:
        member.role && member.role !== 'Developer' && member.role !== 'Reviewer' ? member.role : '',
      providerId: member.providerId,
      model: member.model,
      effort: member.effort,
      mcpPolicy: member.mcpPolicy,
    }))
  ),
  createMemberDraft: vi.fn((member) => member),
  filterEditableMemberInputs: vi.fn((members) => members),
  validateMemberNameInline: vi.fn(() => null),
}));

vi.mock('@renderer/components/team/members/MemberDraftRow', () => ({
  MemberDraftRow: ({
    member,
    lockedRoleLabel,
    lockedModelAction,
  }: {
    member: { name: string };
    lockedRoleLabel?: string;
    lockedModelAction?: {
      label: string;
      onClick: () => void;
    };
  }) =>
    React.createElement(
      'div',
      null,
      member.name,
      lockedRoleLabel ? ` ${lockedRoleLabel}` : '',
      lockedModelAction
        ? React.createElement(
            'button',
            {
              type: 'button',
              'data-testid': 'lead-runtime-action',
              onClick: lockedModelAction.onClick,
            },
            lockedModelAction.label
          )
        : null
    ),
}));

vi.mock('@renderer/components/ui/button', () => ({
  Button: ({
    children,
    onClick,
    type,
    disabled,
  }: {
    children: React.ReactNode;
    onClick?: () => void;
    type?: 'button' | 'submit' | 'reset';
    disabled?: boolean;
  }) => React.createElement('button', { type: type ?? 'button', onClick, disabled }, children),
}));

vi.mock('@renderer/components/ui/dialog', () => ({
  Dialog: ({
    open,
    children,
  }: {
    open: boolean;
    children: React.ReactNode;
  }) => (open ? React.createElement('div', null, children) : null),
  DialogContent: ({ children }: { children: React.ReactNode }) =>
    React.createElement('div', null, children),
  DialogDescription: ({ children }: { children: React.ReactNode }) =>
    React.createElement('div', null, children),
  DialogFooter: ({ children }: { children: React.ReactNode }) =>
    React.createElement('div', null, children),
  DialogHeader: ({ children }: { children: React.ReactNode }) =>
    React.createElement('div', null, children),
  DialogTitle: ({ children }: { children: React.ReactNode }) =>
    React.createElement('div', null, children),
}));

vi.mock('@renderer/hooks/useTheme', () => ({
  useTheme: () => ({ isLight: false }),
}));

vi.mock('@renderer/hooks/useFileListCacheWarmer', () => ({
  useFileListCacheWarmer: () => {},
}));

vi.mock('@renderer/constants/teamColors', () => ({
  getTeamColorSet: () => ({ border: '#22c55e' }),
  getThemedBadge: () => '#0f172a',
}));

import { EditTeamDialog } from '@renderer/components/team/dialogs/EditTeamDialog';
import { api } from '@renderer/api';

describe('EditTeamDialog', () => {
  afterEach(() => {
    document.body.innerHTML = '';
    vi.clearAllMocks();
    vi.unstubAllGlobals();
  });

  it('does not reset unsaved edits when live team props refresh while the dialog stays open', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    const renderDialog = (currentMembers: Array<{ name: string; role?: string }>) =>
      React.createElement(EditTeamDialog, {
        open: true,
        teamName: 'live-team',
        currentName: 'Current Team',
        currentDescription: 'desc',
        currentColor: 'blue',
        currentMembers: currentMembers as any,
        isTeamAlive: true,
        projectPath: '/tmp/project',
        onClose: vi.fn(),
        onChangeLeadRuntime: vi.fn(),
        onSaved: vi.fn(),
      });

    await act(async () => {
      root.render(renderDialog([{ name: 'alice', role: 'Reviewer' }]));
      await Promise.resolve();
    });

    const nameInput = host.querySelector('#edit-team-name') as HTMLInputElement | null;
    expect(nameInput).not.toBeNull();
    if (!nameInput) {
      throw new Error('Expected team name input to exist');
    }

    await act(async () => {
      const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
      setValue?.call(nameInput, 'Unsaved Team Name');
      nameInput.dispatchEvent(new Event('input', { bubbles: true }));
      await Promise.resolve();
    });

    expect(nameInput.value).toBe('Unsaved Team Name');

    await act(async () => {
      root.render(renderDialog([{ name: 'alice', role: 'Developer' }]));
      await Promise.resolve();
    });

    const updatedNameInput = host.querySelector('#edit-team-name') as HTMLInputElement | null;
    expect(updatedNameInput?.value).toBe('Unsaved Team Name');

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('shows the team lead in the members section as read-only context', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(EditTeamDialog, {
          open: true,
          teamName: 'live-team',
          currentName: 'Current Team',
          currentDescription: 'desc',
          currentColor: 'blue',
          currentMembers: [{ name: 'alice', role: 'Reviewer' }] as any,
          leadMember: {
            name: 'team-lead',
            role: 'Team Lead',
            providerId: 'codex',
            model: 'gpt-5.4',
            effort: 'medium',
          } as any,
          resolvedMemberColorMap: new Map([
            ['team-lead', 'forest'],
            ['alice', 'blue'],
          ]),
          isTeamAlive: true,
          projectPath: '/tmp/project',
          onClose: vi.fn(),
          onChangeLeadRuntime: vi.fn(),
          onSaved: vi.fn(),
        })
      );
      await Promise.resolve();
    });

    expect(host.textContent).toContain('lead');
    expect(host.textContent).toContain('Team Lead');
    expect(host.textContent).toContain(
      'Team lead name and role stay read-only here. Open the runtime panel on the lead row to change provider, model, or effort.'
    );

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('blocks saving live roster edits that rename existing teammates', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(EditTeamDialog, {
          open: true,
          teamName: 'live-team',
          currentName: 'Current Team',
          currentDescription: 'desc',
          currentColor: 'blue',
          currentMembers: [
            { name: 'alice', role: 'Reviewer' },
            { name: 'bob', role: 'Developer' },
          ] as any,
          isTeamAlive: true,
          projectPath: '/tmp/project',
          onClose: vi.fn(),
          onChangeLeadRuntime: vi.fn(),
          onSaved: vi.fn(),
        })
      );
      await Promise.resolve();
    });

    const renameButton = host.querySelector('[data-testid=\"rename-existing-member\"]');
    const saveButton = Array.from(host.querySelectorAll('button')).find(
      (button) => button.textContent === 'Save'
    );

    expect(renameButton).not.toBeNull();
    expect(saveButton).not.toBeNull();

    await act(async () => {
      renameButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    await act(async () => {
      saveButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(api.teams.updateConfig).not.toHaveBeenCalled();
    expect(host.textContent).toContain(
      'Live save is blocked because existing teammates were renamed.'
    );

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('saves config-only edits without touching the roster', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    vi.mocked(api.teams.updateConfig).mockResolvedValue({} as any);
    vi.mocked(api.teams.replaceMembers).mockResolvedValue(undefined);
    const onSaved = vi.fn();
    const onClose = vi.fn();

    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(EditTeamDialog, {
          open: true,
          teamName: 'live-team',
          currentName: 'Current Team',
          currentDescription: 'desc',
          currentColor: 'blue',
          currentMembers: [
            { name: 'bob', role: 'Developer', providerId: 'codex', model: 'gpt-5.2' },
            { name: 'alice', role: 'Reviewer', providerId: 'opencode', model: 'openrouter/a' },
          ] as any,
          leadMember: { name: 'team-lead', role: 'Team Lead', providerId: 'codex' } as any,
          isTeamAlive: true,
          projectPath: '/tmp/project',
          onClose,
          onChangeLeadRuntime: vi.fn(),
          onSaved,
        })
      );
      await Promise.resolve();
    });

    const nameInput = host.querySelector('#edit-team-name') as HTMLInputElement;
    await act(async () => {
      const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
      setValue?.call(nameInput, 'Renamed Team');
      nameInput.dispatchEvent(new Event('input', { bubbles: true }));
      await Promise.resolve();
    });

    const saveButton = Array.from(host.querySelectorAll('button')).find(
      (button) => button.textContent === 'Save'
    ) as HTMLButtonElement;
    await act(async () => {
      saveButton.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });
    await act(async () => {
      await Promise.resolve();
    });

    expect(api.teams.updateConfig).toHaveBeenCalledWith('live-team', {
      name: 'Renamed Team',
      description: 'desc',
      color: 'blue',
    });
    expect(api.teams.replaceMembers).not.toHaveBeenCalled();
    expect(api.teams.restartMember).not.toHaveBeenCalled();
    expect(onSaved).toHaveBeenCalled();
    expect(onClose).toHaveBeenCalled();

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('surfaces config-only refresh failures without reporting member changes', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    vi.mocked(api.teams.updateConfig).mockResolvedValue({} as any);
    vi.mocked(api.teams.replaceMembers).mockResolvedValue(undefined);
    const onSaved = vi.fn(async () => {
      throw new Error('refresh failed');
    });
    const onClose = vi.fn();

    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(EditTeamDialog, {
          open: true,
          teamName: 'live-team',
          currentName: 'Current Team',
          currentDescription: 'desc',
          currentColor: 'blue',
          currentMembers: [{ name: 'alice', role: 'Reviewer' }] as any,
          isTeamAlive: true,
          projectPath: '/tmp/project',
          onClose,
          onChangeLeadRuntime: vi.fn(),
          onSaved,
        })
      );
      await Promise.resolve();
    });

    const nameInput = host.querySelector('#edit-team-name') as HTMLInputElement;
    await act(async () => {
      const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
      setValue?.call(nameInput, 'Renamed Team');
      nameInput.dispatchEvent(new Event('input', { bubbles: true }));
      await Promise.resolve();
    });

    const saveButton = Array.from(host.querySelectorAll('button')).find(
      (button) => button.textContent === 'Save'
    ) as HTMLButtonElement;
    await act(async () => {
      saveButton.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });
    await act(async () => {
      await Promise.resolve();
    });

    expect(api.teams.updateConfig).toHaveBeenCalled();
    expect(api.teams.replaceMembers).not.toHaveBeenCalled();
    expect(host.textContent).toContain(
      'Team settings were saved, but failed to refresh the latest view: refresh failed'
    );
    expect(host.textContent).not.toContain('member changes failed');
    expect(onClose).not.toHaveBeenCalled();

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('removes existing live teammates through the dedicated removeMember path during save', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    vi.mocked(api.teams.updateConfig).mockResolvedValue({} as any);
    vi.mocked(api.teams.removeMember).mockResolvedValue(undefined);
    vi.mocked(api.teams.replaceMembers).mockResolvedValue(undefined);

    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(EditTeamDialog, {
          open: true,
          teamName: 'live-team',
          currentName: 'Current Team',
          currentDescription: 'desc',
          currentColor: 'blue',
          currentMembers: [
            { name: 'alice', role: 'Reviewer' },
            { name: 'bob', role: 'Developer' },
          ] as any,
          isTeamAlive: true,
          projectPath: '/tmp/project',
          onClose: vi.fn(),
          onChangeLeadRuntime: vi.fn(),
          onSaved: vi.fn(),
        })
      );
      await Promise.resolve();
    });

    await act(async () => {
      host
        .querySelector('[data-testid="remove-existing-member"]')
        ?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    await act(async () => {
      Array.from(host.querySelectorAll('button'))
        .find((button) => button.textContent === 'Save')
        ?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(api.teams.updateConfig).toHaveBeenCalledTimes(1);
    expect(api.teams.removeMember).toHaveBeenCalledWith('live-team', 'alice');
    expect(api.teams.replaceMembers).toHaveBeenCalledWith('live-team', {
      members: [{ name: 'bob', role: 'Developer', providerId: undefined, model: undefined, effort: undefined }],
    });

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('blocks adding a new teammate from Edit Team while the team is live', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(EditTeamDialog, {
          open: true,
          teamName: 'live-team',
          currentName: 'Current Team',
          currentDescription: 'desc',
          currentColor: 'blue',
          currentMembers: [{ name: 'alice', role: 'Reviewer' }] as any,
          isTeamAlive: true,
          projectPath: '/tmp/project',
          onClose: vi.fn(),
          onChangeLeadRuntime: vi.fn(),
          onSaved: vi.fn(),
        })
      );
      await Promise.resolve();
    });

    const addButton = host.querySelector('[data-testid="add-new-member"]');
    const saveButton = () =>
      Array.from(host.querySelectorAll('button')).find((button) => button.textContent === 'Save');

    await act(async () => {
      addButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    await act(async () => {
      saveButton()?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(api.teams.updateConfig).not.toHaveBeenCalled();
    expect(host.textContent).toContain(
      'New teammates cannot be added from Edit Team while the team is live. Use the Add member dialog instead.'
    );

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('blocks saving while team provisioning is still in progress', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(EditTeamDialog, {
          open: true,
          teamName: 'live-team',
          currentName: 'Current Team',
          currentDescription: 'desc',
          currentColor: 'blue',
          currentMembers: [{ name: 'alice', role: 'Reviewer' }] as any,
          isTeamAlive: false,
          isTeamProvisioning: true,
          projectPath: '/tmp/project',
          onClose: vi.fn(),
          onChangeLeadRuntime: vi.fn(),
          onSaved: vi.fn(),
        })
      );
      await Promise.resolve();
    });

    expect(host.textContent).toContain(
      'Team provisioning is still in progress. Editing is temporarily locked until launch finishes.'
    );

    const saveButton = Array.from(host.querySelectorAll('button')).find(
      (button) => button.textContent === 'Save'
    ) as HTMLButtonElement | undefined;
    expect(saveButton?.disabled).toBe(true);

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('restarts an existing live teammate when role changes', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    vi.mocked(api.teams.updateConfig).mockResolvedValue({} as any);
    vi.mocked(api.teams.replaceMembers).mockResolvedValue(undefined);
    vi.mocked(api.teams.restartMember).mockResolvedValue(undefined);

    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(EditTeamDialog, {
          open: true,
          teamName: 'live-team',
          currentName: 'Current Team',
          currentDescription: 'desc',
          currentColor: 'blue',
          currentMembers: [{ name: 'alice', role: 'Reviewer' }] as any,
          isTeamAlive: true,
          projectPath: '/tmp/project',
          onClose: vi.fn(),
          onChangeLeadRuntime: vi.fn(),
          onSaved: vi.fn(),
        })
      );
      await Promise.resolve();
    });

    await act(async () => {
      host
        .querySelector('[data-testid="change-member-runtime"]')
        ?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(host.textContent).toContain('Saving will restart or relaunch this teammate');

    const saveButton = Array.from(host.querySelectorAll('button')).find(
      (button) => button.textContent === 'Save'
    );

    await act(async () => {
      host
        .querySelector('[data-testid="change-member-role"]')
        ?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    await act(async () => {
      saveButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(api.teams.restartMember).toHaveBeenCalledWith('live-team', 'alice');

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('restarts an existing live teammate when MCP policy changes', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    vi.mocked(api.teams.updateConfig).mockResolvedValue({} as any);
    vi.mocked(api.teams.replaceMembers).mockResolvedValue(undefined);
    vi.mocked(api.teams.restartMember).mockResolvedValue(undefined);

    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(EditTeamDialog, {
          open: true,
          teamName: 'live-team',
          currentName: 'Current Team',
          currentDescription: 'desc',
          currentColor: 'blue',
          currentMembers: [{ name: 'alice', role: 'Developer', providerId: 'codex' }] as any,
          isTeamAlive: true,
          projectPath: '/tmp/project',
          onClose: vi.fn(),
          onChangeLeadRuntime: vi.fn(),
          onSaved: vi.fn(),
        })
      );
      await Promise.resolve();
    });

    await act(async () => {
      host
        .querySelector('[data-testid="change-member-mcp-policy"]')
        ?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(host.textContent).toContain('MCP access changes');

    const saveButton = Array.from(host.querySelectorAll('button')).find(
      (button) => button.textContent === 'Save'
    );

    await act(async () => {
      saveButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(api.teams.replaceMembers).toHaveBeenCalledWith('live-team', {
      members: [
        expect.objectContaining({
          name: 'alice',
          mcpPolicy: { mode: 'appOnly' },
        }),
      ],
    });
    expect(api.teams.restartMember).toHaveBeenCalledWith('live-team', 'alice');

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('does not call generic restart for live OpenCode teammate edits handled by replaceMembers', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    vi.mocked(api.teams.updateConfig).mockResolvedValue({} as any);
    vi.mocked(api.teams.replaceMembers).mockResolvedValue(undefined);
    vi.mocked(api.teams.restartMember).mockResolvedValue(undefined);

    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(EditTeamDialog, {
          open: true,
          teamName: 'live-team',
          currentName: 'Current Team',
          currentDescription: 'desc',
          currentColor: 'blue',
          currentMembers: [
            { name: 'alice', role: 'Reviewer', providerId: 'opencode', model: 'openrouter/a' },
          ] as any,
          isTeamAlive: true,
          projectPath: '/tmp/project',
          onClose: vi.fn(),
          onChangeLeadRuntime: vi.fn(),
          onSaved: vi.fn(),
        })
      );
      await Promise.resolve();
    });

    await act(async () => {
      host
        .querySelector('[data-testid="change-member-role"]')
        ?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    const saveButton = Array.from(host.querySelectorAll('button')).find(
      (button) => button.textContent === 'Save'
    );

    await act(async () => {
      host
        .querySelector('[data-testid="change-member-role"]')
        ?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    await act(async () => {
      saveButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(api.teams.replaceMembers).toHaveBeenCalledWith(
      'live-team',
      expect.objectContaining({
        members: expect.arrayContaining([
          expect.objectContaining({ name: 'alice', providerId: 'opencode' }),
        ]),
      })
    );
    expect(api.teams.restartMember).not.toHaveBeenCalled();

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('blocks live primary-owned teammate edits in mixed OpenCode teams before saving', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    vi.mocked(api.teams.updateConfig).mockResolvedValue({} as any);
    vi.mocked(api.teams.replaceMembers).mockResolvedValue(undefined);
    vi.mocked(api.teams.restartMember).mockResolvedValue(undefined);

    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(EditTeamDialog, {
          open: true,
          teamName: 'live-team',
          currentName: 'Current Team',
          currentDescription: 'desc',
          currentColor: 'blue',
          currentMembers: [
            { name: 'bob', role: 'Reviewer', providerId: 'codex', model: 'gpt-5.2' },
            { name: 'alice', role: 'Reviewer', providerId: 'opencode', model: 'openrouter/a' },
          ] as any,
          leadMember: { name: 'team-lead', role: 'Team Lead', providerId: 'codex' } as any,
          isTeamAlive: true,
          projectPath: '/tmp/project',
          onClose: vi.fn(),
          onChangeLeadRuntime: vi.fn(),
          onSaved: vi.fn(),
        })
      );
      await Promise.resolve();
    });

    await act(async () => {
      host
        .querySelector('[data-testid="change-member-role"]')
        ?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(host.textContent).toContain(
      'Live edits/removals for primary-owned teammates in mixed OpenCode teams'
    );

    const saveButton = Array.from(host.querySelectorAll('button')).find(
      (button) => button.textContent === 'Save'
    ) as HTMLButtonElement | undefined;
    expect(saveButton?.disabled).toBe(true);
    expect(api.teams.updateConfig).not.toHaveBeenCalled();
    expect(api.teams.replaceMembers).not.toHaveBeenCalled();
    expect(api.teams.restartMember).not.toHaveBeenCalled();

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('blocks saving when member names are duplicated', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(EditTeamDialog, {
          open: true,
          teamName: 'live-team',
          currentName: 'Current Team',
          currentDescription: 'desc',
          currentColor: 'blue',
          currentMembers: [
            { name: 'alice', role: 'Reviewer' },
            { name: 'bob', role: 'Developer' },
          ] as any,
          isTeamAlive: false,
          projectPath: '/tmp/project',
          onClose: vi.fn(),
          onChangeLeadRuntime: vi.fn(),
          onSaved: vi.fn(),
        })
      );
      await Promise.resolve();
    });

    const duplicateButton = host.querySelector('[data-testid=\"duplicate-member-name\"]');
    const saveButton = Array.from(host.querySelectorAll('button')).find(
      (button) => button.textContent === 'Save'
    );

    await act(async () => {
      duplicateButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect((saveButton as HTMLButtonElement | undefined)?.disabled).toBe(true);
    expect(api.teams.updateConfig).not.toHaveBeenCalled();

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('clears stale validation feedback after the user edits the form', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(EditTeamDialog, {
          open: true,
          teamName: 'live-team',
          currentName: 'Current Team',
          currentDescription: 'desc',
          currentColor: 'blue',
          currentMembers: [{ name: 'alice', role: 'Reviewer' }] as any,
          isTeamAlive: true,
          projectPath: '/tmp/project',
          onClose: vi.fn(),
          onChangeLeadRuntime: vi.fn(),
          onSaved: vi.fn(),
        })
      );
      await Promise.resolve();
    });

    const addButton = host.querySelector('[data-testid="add-new-member"]');
    const removeButton = host.querySelector('[data-testid="remove-new-member"]');
    const saveButton = () =>
      Array.from(host.querySelectorAll('button')).find((button) => button.textContent === 'Save');

    await act(async () => {
      addButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    await act(async () => {
      saveButton()?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(host.textContent).toContain(
      'New teammates cannot be added from Edit Team while the team is live. Use the Add member dialog instead.'
    );

    await act(async () => {
      removeButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(host.textContent).not.toContain(
      'New teammates cannot be added from Edit Team while the team is live. Use the Add member dialog instead.'
    );

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('surfaces partial-save feedback when team settings save but member changes fail', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    vi.mocked(api.teams.updateConfig).mockResolvedValue({} as any);
    vi.mocked(api.teams.replaceMembers).mockRejectedValueOnce(new Error('disk write failed'));
    const onSaved = vi.fn();

    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(EditTeamDialog, {
          open: true,
          teamName: 'live-team',
          currentName: 'Current Team',
          currentDescription: 'desc',
          currentColor: 'blue',
          currentMembers: [{ name: 'alice', role: 'Reviewer' }] as any,
          isTeamAlive: true,
          projectPath: '/tmp/project',
          onClose: vi.fn(),
          onChangeLeadRuntime: vi.fn(),
          onSaved,
        })
      );
      await Promise.resolve();
    });

    const saveButton = Array.from(host.querySelectorAll('button')).find(
      (button) => button.textContent === 'Save'
    );

    await act(async () => {
      host
        .querySelector('[data-testid="change-member-role"]')
        ?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    await act(async () => {
      saveButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(host.textContent).toContain(
      'Team settings were saved, but member changes failed: disk write failed'
    );
    expect(onSaved).toHaveBeenCalledTimes(1);

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('allows retrying save after config-only partial save once refreshed settings props catch up', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    vi.mocked(api.teams.updateConfig).mockResolvedValue({} as any);
    vi.mocked(api.teams.replaceMembers)
      .mockRejectedValueOnce(new Error('disk write failed'))
      .mockResolvedValueOnce(undefined);

    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    const renderDialog = (currentName: string) =>
      React.createElement(EditTeamDialog, {
        open: true,
        teamName: 'live-team',
        currentName,
        currentDescription: 'desc',
        currentColor: 'blue',
        currentMembers: [{ name: 'alice', role: 'Reviewer' }] as any,
        isTeamAlive: true,
        projectPath: '/tmp/project',
        onClose: vi.fn(),
        onChangeLeadRuntime: vi.fn(),
        onSaved: vi.fn(),
      });

    await act(async () => {
      root.render(renderDialog('Current Team'));
      await Promise.resolve();
    });

    const nameInput = host.querySelector('#edit-team-name') as HTMLInputElement | null;
    const saveButton = () =>
      Array.from(host.querySelectorAll('button')).find((button) => button.textContent === 'Save');

    await act(async () => {
      const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
      setValue?.call(nameInput, 'Renamed Team');
      nameInput?.dispatchEvent(new Event('input', { bubbles: true }));
      await Promise.resolve();
    });

    await act(async () => {
      host
        .querySelector('[data-testid="change-member-role"]')
        ?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    await act(async () => {
      saveButton()?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(host.textContent).toContain(
      'Team settings were saved, but member changes failed: disk write failed'
    );

    await act(async () => {
      root.render(renderDialog('Renamed Team'));
      await Promise.resolve();
    });

    await act(async () => {
      saveButton()?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(host.textContent).not.toContain(
      'Team settings changed while this dialog was open. Reopen it and review the latest state before saving.'
    );
    expect(api.teams.updateConfig).toHaveBeenCalledTimes(2);

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('blocks saving when a teammate name is reserved', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(EditTeamDialog, {
          open: true,
          teamName: 'live-team',
          currentName: 'Current Team',
          currentDescription: 'desc',
          currentColor: 'blue',
          currentMembers: [
            { name: 'alice', role: 'Reviewer' },
            { name: 'bob', role: 'Developer' },
          ] as any,
          isTeamAlive: false,
          projectPath: '/tmp/project',
          onClose: vi.fn(),
          onChangeLeadRuntime: vi.fn(),
          onSaved: vi.fn(),
        })
      );
      await Promise.resolve();
    });

    const invalidButton = host.querySelector('[data-testid=\"invalid-member-name\"]');
    const saveButton = Array.from(host.querySelectorAll('button')).find(
      (button) => button.textContent === 'Save'
    );

    await act(async () => {
      invalidButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect((saveButton as HTMLButtonElement | undefined)?.disabled).toBe(true);
    expect(host.querySelector('[data-testid="members-field-error"]')?.textContent).toContain(
      'Member name "team-lead" is reserved'
    );
    expect(api.teams.updateConfig).not.toHaveBeenCalled();

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('blocks saving when editable team source data changed while the dialog stayed open', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    const renderDialog = (role: string) =>
      React.createElement(EditTeamDialog, {
        open: true,
        teamName: 'live-team',
        currentName: 'Current Team',
        currentDescription: 'desc',
        currentColor: 'blue',
        currentMembers: [{ name: 'alice', role }] as any,
        isTeamAlive: true,
        projectPath: '/tmp/project',
        onClose: vi.fn(),
        onChangeLeadRuntime: vi.fn(),
        onSaved: vi.fn(),
      });

    await act(async () => {
      root.render(renderDialog('Reviewer'));
      await Promise.resolve();
    });

    await act(async () => {
      root.render(renderDialog('Developer'));
      await Promise.resolve();
    });

    const saveButton = Array.from(host.querySelectorAll('button')).find(
      (button) => button.textContent === 'Save'
    );

    await act(async () => {
      saveButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(api.teams.updateConfig).not.toHaveBeenCalled();
    expect(host.textContent).toContain(
      'Team settings changed while this dialog was open. Reopen it and review the latest state before saving.'
    );

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('allows retrying save after restart failures before props catch up to the committed state', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    vi.mocked(api.teams.updateConfig).mockResolvedValue({} as any);
    vi.mocked(api.teams.replaceMembers).mockResolvedValue(undefined);
    vi.mocked(api.teams.restartMember)
      .mockRejectedValueOnce(new Error('restart failed'))
      .mockResolvedValueOnce(undefined);

    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);
    const onSaved = vi.fn();

    const renderDialog = (role: string) =>
      React.createElement(EditTeamDialog, {
        open: true,
        teamName: 'live-team',
        currentName: 'Current Team',
        currentDescription: 'desc',
        currentColor: 'blue',
        currentMembers: [{ name: 'alice', role, providerId: 'codex', model: 'gpt-5.2' }] as any,
        isTeamAlive: true,
        projectPath: '/tmp/project',
        onClose: vi.fn(),
        onChangeLeadRuntime: vi.fn(),
        onSaved,
      });

    await act(async () => {
      root.render(renderDialog('Reviewer'));
      await Promise.resolve();
    });

    const saveButton = () =>
      Array.from(host.querySelectorAll('button')).find((button) => button.textContent === 'Save');

    await act(async () => {
      host
        .querySelector('[data-testid="change-member-runtime"]')
        ?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    await act(async () => {
      saveButton()?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(api.teams.restartMember).toHaveBeenCalledTimes(1);

    await act(async () => {
      root.render(
        React.createElement(EditTeamDialog, {
          open: true,
          teamName: 'live-team',
          currentName: 'Current Team',
          currentDescription: 'desc',
          currentColor: 'blue',
          currentMembers: [{ name: 'alice', role: 'Reviewer', providerId: 'codex', model: 'gpt-5.4' }] as any,
          isTeamAlive: true,
          projectPath: '/tmp/project',
          onClose: vi.fn(),
          onChangeLeadRuntime: vi.fn(),
          onSaved,
        })
      );
      await Promise.resolve();
    });

    await act(async () => {
      saveButton()?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(host.textContent).not.toContain(
      'Team settings changed while this dialog was open. Reopen it and review the latest state before saving.'
    );
    expect(api.teams.updateConfig).toHaveBeenCalledTimes(2);
    expect(api.teams.restartMember).toHaveBeenCalledTimes(2);

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('drops pending restart retry when the member runtime is changed away from the failed target', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    vi.mocked(api.teams.updateConfig).mockResolvedValue({} as any);
    vi.mocked(api.teams.replaceMembers).mockResolvedValue(undefined);
    vi.mocked(api.teams.restartMember).mockRejectedValueOnce(new Error('restart failed'));

    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(EditTeamDialog, {
          open: true,
          teamName: 'live-team',
          currentName: 'Current Team',
          currentDescription: 'desc',
          currentColor: 'blue',
          currentMembers: [{ name: 'alice', role: 'Reviewer', providerId: 'codex', model: 'gpt-5.2' }] as any,
          isTeamAlive: true,
          projectPath: '/tmp/project',
          onClose: vi.fn(),
          onChangeLeadRuntime: vi.fn(),
          onSaved: vi.fn(),
        })
      );
      await Promise.resolve();
    });

    const saveButton = () =>
      Array.from(host.querySelectorAll('button')).find((button) => button.textContent === 'Save');

    await act(async () => {
      host
        .querySelector('[data-testid="change-member-runtime"]')
        ?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    await act(async () => {
      saveButton()?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(api.teams.restartMember).toHaveBeenCalledTimes(1);

    await act(async () => {
      host
        .querySelector('[data-testid="revert-member-runtime"]')
        ?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    await act(async () => {
      saveButton()?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(api.teams.restartMember).toHaveBeenCalledTimes(1);

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('shows an inline lead runtime action inside the lead context row', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);

    const onChangeLeadRuntime = vi.fn();
    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(EditTeamDialog, {
          open: true,
          teamName: 'team-alpha',
          currentName: 'Team Alpha',
          currentDescription: 'desc',
          currentColor: 'blue',
          currentMembers: [{ name: 'alice', role: 'Reviewer' }] as any,
          leadMember: {
            name: 'lead',
            role: 'Team Lead',
            providerId: 'codex',
            model: 'gpt-5.4',
            effort: 'medium',
          } as any,
          projectPath: '/tmp/project',
          onClose: vi.fn(),
          onChangeLeadRuntime,
          onSaved: vi.fn(),
        })
      );
      await Promise.resolve();
    });

    const button = host.querySelector('[data-testid="lead-runtime-action"]');
    expect(button).toBeTruthy();

    await act(async () => {
      button?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(onChangeLeadRuntime).toHaveBeenCalledTimes(1);

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });
});
