import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom';

import { CreateBranchModal } from '../git/CreateBranchModal';
import { DeleteBranchModal } from '../git/DeleteBranchModal';
import { SyncToGitModal } from '../git/SyncToGitModal';
import { GitSettingsModal } from '../git/GitSettingsModal';
import { CreatePRModal } from '../git/CreatePRModal';

describe('<CreateBranchModal />', () => {
  const defaultProps = {
    isOpen: true,
    onClose: vi.fn(),
    onCreate: vi.fn(),
    namespace: 'analytics.prod',
    gitBranch: 'main',
  };

  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should not render when isOpen is false', () => {
    render(<CreateBranchModal {...defaultProps} isOpen={false} />);
    expect(screen.queryByText('Create Branch')).not.toBeInTheDocument();
  });

  it('should render the modal when open', () => {
    render(<CreateBranchModal {...defaultProps} />);
    // Use role to distinguish header from button
    expect(
      screen.getByRole('heading', { name: 'Create Branch' }),
    ).toBeInTheDocument();
    expect(screen.getByLabelText('Branch Name')).toBeInTheDocument();
  });

  it('should show preview when branch name is entered', async () => {
    render(<CreateBranchModal {...defaultProps} />);

    const input = screen.getByLabelText('Branch Name');
    await userEvent.type(input, 'feature-xyz');

    expect(
      screen.getByText(/Create git branch "feature-xyz"/),
    ).toBeInTheDocument();
    expect(screen.getByText(/analytics\.feature_xyz/)).toBeInTheDocument();
  });

  it('should handle branch name with slashes', async () => {
    render(<CreateBranchModal {...defaultProps} />);

    const input = screen.getByLabelText('Branch Name');
    await userEvent.type(input, 'feature/new-thing');

    // Slashes should be converted to underscores in namespace preview
    expect(
      screen.getByText(/analytics\.feature_new_thing/),
    ).toBeInTheDocument();
  });

  it('should disable submit button when branch name is empty', () => {
    render(<CreateBranchModal {...defaultProps} />);

    const submitButton = screen.getByRole('button', { name: 'Create Branch' });
    expect(submitButton).toBeDisabled();
  });

  it('should disable submit button when branch name is whitespace only', async () => {
    render(<CreateBranchModal {...defaultProps} />);

    const input = screen.getByLabelText('Branch Name');
    await userEvent.type(input, '   ');

    const submitButton = screen.getByRole('button', { name: 'Create Branch' });
    expect(submitButton).toBeDisabled();
  });

  it('should call onCreate when form is submitted', async () => {
    defaultProps.onCreate.mockResolvedValue({
      branch: {
        namespace: 'analytics.feature_xyz',
        git_branch: 'feature-xyz',
        parent_namespace: 'analytics.prod',
      },
      deployment_results: [],
    });

    render(<CreateBranchModal {...defaultProps} />);

    const input = screen.getByLabelText('Branch Name');
    await userEvent.type(input, 'feature-xyz');

    const submitButton = screen.getByRole('button', { name: 'Create Branch' });
    await userEvent.click(submitButton);

    await waitFor(() => {
      expect(defaultProps.onCreate).toHaveBeenCalledWith('feature-xyz');
    });
  });

  it('should show success view after branch creation', async () => {
    defaultProps.onCreate.mockResolvedValue({
      branch: {
        namespace: 'analytics.feature_xyz',
        git_branch: 'feature-xyz',
        parent_namespace: 'analytics.prod',
      },
      deployment_results: [{ node: 'test' }],
    });

    render(
      <MemoryRouter>
        <CreateBranchModal {...defaultProps} />
      </MemoryRouter>,
    );

    const input = screen.getByLabelText('Branch Name');
    await userEvent.type(input, 'feature-xyz');
    await userEvent.click(
      screen.getByRole('button', { name: 'Create Branch' }),
    );

    await waitFor(() => {
      expect(screen.getByText('Branch Created!')).toBeInTheDocument();
    });
    expect(screen.getByText('analytics.feature_xyz')).toBeInTheDocument();
    expect(screen.getByText('Nodes copied:')).toBeInTheDocument();
  });

  it('should show success view without nodes copied section when no deployment results', async () => {
    defaultProps.onCreate.mockResolvedValue({
      branch: {
        namespace: 'analytics.feature_xyz',
        git_branch: 'feature-xyz',
        parent_namespace: 'analytics.prod',
      },
      deployment_results: [],
    });

    render(
      <MemoryRouter>
        <CreateBranchModal {...defaultProps} />
      </MemoryRouter>,
    );

    const input = screen.getByLabelText('Branch Name');
    await userEvent.type(input, 'feature-xyz');
    await userEvent.click(
      screen.getByRole('button', { name: 'Create Branch' }),
    );

    await waitFor(() => {
      expect(screen.getByText('Branch Created!')).toBeInTheDocument();
    });
    expect(screen.queryByText('Nodes copied:')).not.toBeInTheDocument();
  });

  it('should show error when creation fails', async () => {
    defaultProps.onCreate.mockResolvedValue({
      _error: true,
      message: 'Branch already exists',
    });

    render(<CreateBranchModal {...defaultProps} />);

    const input = screen.getByLabelText('Branch Name');
    await userEvent.type(input, 'feature-xyz');
    await userEvent.click(
      screen.getByRole('button', { name: 'Create Branch' }),
    );

    await waitFor(() => {
      expect(screen.getByText('Branch already exists')).toBeInTheDocument();
    });
  });

  it('should show error when onCreate throws exception', async () => {
    defaultProps.onCreate.mockRejectedValue(new Error('Network error'));

    render(<CreateBranchModal {...defaultProps} />);

    const input = screen.getByLabelText('Branch Name');
    await userEvent.type(input, 'feature-xyz');
    await userEvent.click(
      screen.getByRole('button', { name: 'Create Branch' }),
    );

    await waitFor(() => {
      expect(screen.getByText('Network error')).toBeInTheDocument();
    });
  });

  it('should show default error when onCreate throws without message', async () => {
    defaultProps.onCreate.mockRejectedValue({});

    render(<CreateBranchModal {...defaultProps} />);

    const input = screen.getByLabelText('Branch Name');
    await userEvent.type(input, 'feature-xyz');
    await userEvent.click(
      screen.getByRole('button', { name: 'Create Branch' }),
    );

    await waitFor(() => {
      expect(screen.getByText('Failed to create branch')).toBeInTheDocument();
    });
  });

  it('should call onClose when Cancel is clicked', async () => {
    render(<CreateBranchModal {...defaultProps} />);

    await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
    expect(defaultProps.onClose).toHaveBeenCalled();
  });

  it('should call onClose when clicking overlay', async () => {
    render(<CreateBranchModal {...defaultProps} />);

    const overlay = document.querySelector('.modal-overlay');
    fireEvent.click(overlay);
    expect(defaultProps.onClose).toHaveBeenCalled();
  });

  it('should call onClose when clicking close button', async () => {
    render(<CreateBranchModal {...defaultProps} />);

    await userEvent.click(screen.getByTitle('Close'));
    expect(defaultProps.onClose).toHaveBeenCalled();
  });

  it('should not propagate click from modal content to overlay', async () => {
    render(<CreateBranchModal {...defaultProps} />);

    const modalContent = document.querySelector('.modal-content');
    fireEvent.click(modalContent);
    expect(defaultProps.onClose).not.toHaveBeenCalled();
  });

  it('should reset state when closed from success view', async () => {
    defaultProps.onCreate.mockResolvedValue({
      branch: {
        namespace: 'analytics.feature_xyz',
        git_branch: 'feature-xyz',
        parent_namespace: 'analytics.prod',
      },
      deployment_results: [],
    });

    render(
      <MemoryRouter>
        <CreateBranchModal {...defaultProps} />
      </MemoryRouter>,
    );

    const input = screen.getByLabelText('Branch Name');
    await userEvent.type(input, 'feature-xyz');
    await userEvent.click(
      screen.getByRole('button', { name: 'Create Branch' }),
    );

    await waitFor(() => {
      expect(screen.getByText('Branch Created!')).toBeInTheDocument();
    });

    // Click Close button in success view
    await userEvent.click(screen.getByRole('button', { name: 'Close' }));
    expect(defaultProps.onClose).toHaveBeenCalled();
  });

  it('should show Creating... button text while creating', async () => {
    let resolveCreate;
    defaultProps.onCreate.mockImplementation(
      () =>
        new Promise(resolve => {
          resolveCreate = resolve;
        }),
    );

    render(<CreateBranchModal {...defaultProps} />);

    const input = screen.getByLabelText('Branch Name');
    await userEvent.type(input, 'feature-xyz');
    await userEvent.click(
      screen.getByRole('button', { name: 'Create Branch' }),
    );

    expect(screen.getByText('Creating...')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: 'Creating...' })).toBeDisabled();

    // Resolve to complete the test
    resolveCreate({
      branch: { namespace: 'test', git_branch: 'test', parent_namespace: 'p' },
      deployment_results: [],
    });
  });

  it('should handle single-part namespace', async () => {
    render(<CreateBranchModal {...defaultProps} namespace="analytics" />);

    const input = screen.getByLabelText('Branch Name');
    await userEvent.type(input, 'feature');

    // For single-part namespace, result should be "analytics.feature"
    expect(screen.getByText(/analytics\.feature/)).toBeInTheDocument();
  });
});

describe('<DeleteBranchModal />', () => {
  const defaultProps = {
    isOpen: true,
    onClose: vi.fn(),
    onDelete: vi.fn(),
    namespace: 'analytics.feature_xyz',
    gitBranch: 'feature-xyz',
    parentNamespace: 'analytics.prod',
  };

  beforeEach(() => {
    vi.clearAllMocks();
    delete window.location;
    window.location = { href: '' };
  });

  it('should not render when isOpen is false', () => {
    render(<DeleteBranchModal {...defaultProps} isOpen={false} />);
    expect(screen.queryByText('Delete Branch')).not.toBeInTheDocument();
  });

  it('should render the modal with branch info', () => {
    render(<DeleteBranchModal {...defaultProps} />);

    // Use role to distinguish header from button
    expect(
      screen.getByRole('heading', { name: 'Delete Branch' }),
    ).toBeInTheDocument();
    expect(screen.getByText('analytics.feature_xyz')).toBeInTheDocument();
    expect(screen.getByText('feature-xyz')).toBeInTheDocument();
    expect(screen.getByText('analytics.prod')).toBeInTheDocument();
  });

  it('should have checkbox for deleting git branch checked by default', () => {
    render(<DeleteBranchModal {...defaultProps} />);

    const checkbox = screen.getByRole('checkbox');
    expect(checkbox).toBeChecked();
  });

  it('should toggle checkbox', async () => {
    render(<DeleteBranchModal {...defaultProps} />);

    const checkbox = screen.getByRole('checkbox');
    await userEvent.click(checkbox);
    expect(checkbox).not.toBeChecked();
  });

  it('should call onDelete with checkbox value when submitted', async () => {
    defaultProps.onDelete.mockResolvedValue({ success: true });

    render(<DeleteBranchModal {...defaultProps} />);

    await userEvent.click(
      screen.getByRole('button', { name: 'Delete Branch' }),
    );

    await waitFor(() => {
      expect(defaultProps.onDelete).toHaveBeenCalledWith(true);
    });
  });

  it('should call onDelete with false when checkbox is unchecked', async () => {
    defaultProps.onDelete.mockResolvedValue({ success: true });

    render(<DeleteBranchModal {...defaultProps} />);

    // Uncheck the checkbox
    await userEvent.click(screen.getByRole('checkbox'));

    await userEvent.click(
      screen.getByRole('button', { name: 'Delete Branch' }),
    );

    await waitFor(() => {
      expect(defaultProps.onDelete).toHaveBeenCalledWith(false);
    });
  });

  it('should redirect after successful deletion', async () => {
    defaultProps.onDelete.mockResolvedValue({ success: true });

    render(<DeleteBranchModal {...defaultProps} />);

    await userEvent.click(
      screen.getByRole('button', { name: 'Delete Branch' }),
    );

    await waitFor(() => {
      expect(window.location.href).toBe('/namespaces/analytics.prod');
    });
  });

  it('should show error when deletion fails', async () => {
    defaultProps.onDelete.mockResolvedValue({
      _error: true,
      message: 'Cannot delete branch with open PR',
    });

    render(<DeleteBranchModal {...defaultProps} />);

    await userEvent.click(
      screen.getByRole('button', { name: 'Delete Branch' }),
    );

    await waitFor(() => {
      expect(
        screen.getByText('Cannot delete branch with open PR'),
      ).toBeInTheDocument();
    });
  });

  it('should show error when onDelete throws exception', async () => {
    defaultProps.onDelete.mockRejectedValue(new Error('Network failure'));

    render(<DeleteBranchModal {...defaultProps} />);

    await userEvent.click(
      screen.getByRole('button', { name: 'Delete Branch' }),
    );

    await waitFor(() => {
      expect(screen.getByText('Network failure')).toBeInTheDocument();
    });
  });

  it('should show default error when onDelete throws without message', async () => {
    defaultProps.onDelete.mockRejectedValue({});

    render(<DeleteBranchModal {...defaultProps} />);

    await userEvent.click(
      screen.getByRole('button', { name: 'Delete Branch' }),
    );

    await waitFor(() => {
      expect(screen.getByText('Failed to delete branch')).toBeInTheDocument();
    });
  });

  it('should call onClose when Cancel is clicked', async () => {
    render(<DeleteBranchModal {...defaultProps} />);

    await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
    expect(defaultProps.onClose).toHaveBeenCalled();
  });

  it('should call onClose when clicking overlay', async () => {
    render(<DeleteBranchModal {...defaultProps} />);

    const overlay = document.querySelector('.modal-overlay');
    fireEvent.click(overlay);
    expect(defaultProps.onClose).toHaveBeenCalled();
  });

  it('should call onClose when clicking close button', async () => {
    render(<DeleteBranchModal {...defaultProps} />);

    await userEvent.click(screen.getByTitle('Close'));
    expect(defaultProps.onClose).toHaveBeenCalled();
  });

  it('should show Deleting... button text while deleting', async () => {
    let resolveDelete;
    defaultProps.onDelete.mockImplementation(
      () =>
        new Promise(resolve => {
          resolveDelete = resolve;
        }),
    );

    render(<DeleteBranchModal {...defaultProps} />);

    await userEvent.click(
      screen.getByRole('button', { name: 'Delete Branch' }),
    );

    expect(screen.getByText('Deleting...')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: 'Deleting...' })).toBeDisabled();

    // Resolve to complete the test
    resolveDelete({ success: true });
  });
});

describe('<SyncToGitModal />', () => {
  const defaultProps = {
    isOpen: true,
    onClose: vi.fn(),
    onSync: vi.fn(),
    namespace: 'analytics.prod',
    gitBranch: 'main',
    repoPath: 'myorg/dj-definitions',
  };

  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should not render when isOpen is false', () => {
    render(<SyncToGitModal {...defaultProps} isOpen={false} />);
    expect(screen.queryByText('Sync to Git')).not.toBeInTheDocument();
  });

  it('should render the modal with namespace info', () => {
    render(<SyncToGitModal {...defaultProps} />);

    expect(screen.getByText('Sync to Git')).toBeInTheDocument();
    expect(screen.getByText('myorg/dj-definitions')).toBeInTheDocument();
    expect(screen.getByText('main')).toBeInTheDocument();
    expect(screen.getByText(/Sync all nodes in/)).toBeInTheDocument();
    expect(screen.getByText('analytics.prod')).toBeInTheDocument();
  });

  it('should allow entering commit message', async () => {
    render(<SyncToGitModal {...defaultProps} />);

    const input = screen.getByLabelText(/Commit Message/);
    await userEvent.type(input, 'Add new metrics');

    expect(input).toHaveValue('Add new metrics');
  });

  it('should call onSync when form is submitted', async () => {
    defaultProps.onSync.mockResolvedValue({
      files_synced: 5,
      commit_sha: 'abc123def',
      commit_url: 'https://github.com/myorg/repo/commit/abc123',
    });

    render(<SyncToGitModal {...defaultProps} />);

    const input = screen.getByLabelText(/Commit Message/);
    await userEvent.type(input, 'Update metrics');
    await userEvent.click(screen.getByRole('button', { name: 'Sync Now' }));

    await waitFor(() => {
      expect(defaultProps.onSync).toHaveBeenCalledWith('Update metrics');
    });
  });

  it('should call onSync with null for empty commit message', async () => {
    defaultProps.onSync.mockResolvedValue({
      files_synced: 3,
      commit_sha: 'abc123',
      commit_url: 'https://github.com/myorg/repo/commit/abc123',
    });

    render(<SyncToGitModal {...defaultProps} />);
    await userEvent.click(screen.getByRole('button', { name: 'Sync Now' }));

    await waitFor(() => {
      expect(defaultProps.onSync).toHaveBeenCalledWith(null);
    });
  });

  it('should call onSync with null for whitespace-only commit message', async () => {
    defaultProps.onSync.mockResolvedValue({
      files_synced: 3,
      commit_sha: 'abc123',
      commit_url: 'https://github.com/myorg/repo/commit/abc123',
    });

    render(<SyncToGitModal {...defaultProps} />);

    const input = screen.getByLabelText(/Commit Message/);
    await userEvent.type(input, '   ');
    await userEvent.click(screen.getByRole('button', { name: 'Sync Now' }));

    await waitFor(() => {
      expect(defaultProps.onSync).toHaveBeenCalledWith(null);
    });
  });

  it('should show success view after sync', async () => {
    defaultProps.onSync.mockResolvedValue({
      files_synced: 5,
      commit_sha: 'abc123def456',
      commit_url: 'https://github.com/myorg/repo/commit/abc123',
    });

    render(<SyncToGitModal {...defaultProps} />);
    await userEvent.click(screen.getByRole('button', { name: 'Sync Now' }));

    await waitFor(() => {
      expect(screen.getByText('Synced 5 files!')).toBeInTheDocument();
    });
    expect(screen.getByText('abc123d')).toBeInTheDocument();
    expect(screen.getByRole('link', { name: 'View Commit' })).toHaveAttribute(
      'href',
      'https://github.com/myorg/repo/commit/abc123',
    );
  });

  it('should show singular "file" for 1 file synced', async () => {
    defaultProps.onSync.mockResolvedValue({
      files_synced: 1,
      commit_sha: 'abc123',
      commit_url: 'https://github.com/myorg/repo/commit/abc123',
    });

    render(<SyncToGitModal {...defaultProps} />);
    await userEvent.click(screen.getByRole('button', { name: 'Sync Now' }));

    await waitFor(() => {
      expect(screen.getByText('Synced 1 file!')).toBeInTheDocument();
    });
  });

  it('should show error when sync fails', async () => {
    defaultProps.onSync.mockResolvedValue({
      _error: true,
      message: 'No nodes to sync',
    });

    render(<SyncToGitModal {...defaultProps} />);
    await userEvent.click(screen.getByRole('button', { name: 'Sync Now' }));

    await waitFor(() => {
      expect(screen.getByText('No nodes to sync')).toBeInTheDocument();
    });
  });

  it('should show error when onSync throws exception', async () => {
    defaultProps.onSync.mockRejectedValue(new Error('GitHub API error'));

    render(<SyncToGitModal {...defaultProps} />);
    await userEvent.click(screen.getByRole('button', { name: 'Sync Now' }));

    await waitFor(() => {
      expect(screen.getByText('GitHub API error')).toBeInTheDocument();
    });
  });

  it('should show default error when onSync throws without message', async () => {
    defaultProps.onSync.mockRejectedValue({});

    render(<SyncToGitModal {...defaultProps} />);
    await userEvent.click(screen.getByRole('button', { name: 'Sync Now' }));

    await waitFor(() => {
      expect(screen.getByText('Failed to sync to git')).toBeInTheDocument();
    });
  });

  it('should call onClose when Cancel is clicked', async () => {
    render(<SyncToGitModal {...defaultProps} />);

    await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
    expect(defaultProps.onClose).toHaveBeenCalled();
  });

  it('should call onClose when clicking overlay', async () => {
    render(<SyncToGitModal {...defaultProps} />);

    const overlay = document.querySelector('.modal-overlay');
    fireEvent.click(overlay);
    expect(defaultProps.onClose).toHaveBeenCalled();
  });

  it('should call onClose when clicking close button', async () => {
    render(<SyncToGitModal {...defaultProps} />);

    await userEvent.click(screen.getByTitle('Close'));
    expect(defaultProps.onClose).toHaveBeenCalled();
  });

  it('should reset state when closed from success view', async () => {
    defaultProps.onSync.mockResolvedValue({
      files_synced: 5,
      commit_sha: 'abc123',
      commit_url: 'https://github.com/myorg/repo/commit/abc123',
    });

    render(<SyncToGitModal {...defaultProps} />);
    await userEvent.click(screen.getByRole('button', { name: 'Sync Now' }));

    await waitFor(() => {
      expect(screen.getByText('Synced 5 files!')).toBeInTheDocument();
    });

    // Click Close button in success view
    await userEvent.click(screen.getByRole('button', { name: 'Close' }));
    expect(defaultProps.onClose).toHaveBeenCalled();
  });

  it('should show Syncing... button text while syncing', async () => {
    let resolveSync;
    defaultProps.onSync.mockImplementation(
      () =>
        new Promise(resolve => {
          resolveSync = resolve;
        }),
    );

    render(<SyncToGitModal {...defaultProps} />);
    await userEvent.click(screen.getByRole('button', { name: 'Sync Now' }));

    expect(screen.getByText('Syncing...')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: 'Syncing...' })).toBeDisabled();

    // Resolve to complete the test
    resolveSync({
      files_synced: 1,
      commit_sha: 'abc',
      commit_url: 'https://github.com/test',
    });
  });

  it('should display branch info in success view', async () => {
    defaultProps.onSync.mockResolvedValue({
      files_synced: 5,
      commit_sha: 'abc123',
      commit_url: 'https://github.com/myorg/repo/commit/abc123',
    });

    render(<SyncToGitModal {...defaultProps} />);
    await userEvent.click(screen.getByRole('button', { name: 'Sync Now' }));

    await waitFor(() => {
      expect(screen.getByText('Synced 5 files!')).toBeInTheDocument();
    });

    // Branch info should be shown
    expect(screen.getByText('Branch:')).toBeInTheDocument();
    expect(screen.getByText('main')).toBeInTheDocument();
  });
});

describe('<GitSettingsModal />', () => {
  const defaultProps = {
    isOpen: true,
    onClose: vi.fn(),
    onSave: vi.fn(),
    onRemove: vi.fn(),
    currentConfig: null,
    namespace: 'analytics.prod',
    nodeCount: 0,
  };

  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should not render when isOpen is false', () => {
    render(<GitSettingsModal {...defaultProps} isOpen={false} />);
    expect(screen.queryByText('Git Configuration')).not.toBeInTheDocument();
  });

  it('should render the modal heading and source toggle', () => {
    render(<GitSettingsModal {...defaultProps} />);

    expect(
      screen.getByRole('heading', { name: 'Git Configuration' }),
    ).toBeInTheDocument();
    // Source of definitions toggle is always visible
    expect(screen.getByText('Edited in DataJunction')).toBeInTheDocument();
    expect(screen.getByText('Git repo')).toBeInTheDocument();
    // By default, source is "dj" so git-specific fields are hidden
    expect(screen.queryByLabelText(/Repository/)).not.toBeInTheDocument();
  });

  it('should show Repository and Branch fields when Git repo is selected', async () => {
    render(<GitSettingsModal {...defaultProps} />);

    await userEvent.click(screen.getByText('Git repo'));

    expect(screen.getByLabelText(/Repository/)).toBeInTheDocument();
    expect(screen.getByLabelText(/^Branch$/)).toBeInTheDocument();
    // No checkbox — intent-based modal has no git_only toggle
    expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
  });

  it('should show Default branch field in root (Feature branches + PRs) mode', async () => {
    render(<GitSettingsModal {...defaultProps} />);

    await userEvent.click(screen.getByText('Git repo'));
    await userEvent.click(screen.getByText('Feature branches + PRs'));

    expect(screen.getByLabelText(/Default branch/)).toBeInTheDocument();
    expect(screen.queryByLabelText(/^Branch$/)).not.toBeInTheDocument();
  });

  it('should preselect flat mode from an existing flat (branch-tracking) config', () => {
    render(
      <GitSettingsModal
        {...defaultProps}
        currentConfig={{
          github_repo_path: 'myorg/repo',
          git_branch: 'develop',
        }}
      />,
    );

    // Source should default to "git", flat mode active
    expect(screen.getByDisplayValue('myorg/repo')).toBeInTheDocument();
    expect(screen.getByDisplayValue('develop')).toBeInTheDocument();
    expect(screen.getByText('Tracks a branch')).toBeInTheDocument();
  });

  it('should preselect root mode from an existing root config', () => {
    render(
      <GitSettingsModal
        {...defaultProps}
        currentConfig={{
          github_repo_path: 'myorg/repo',
          default_branch: 'main',
        }}
      />,
    );

    expect(screen.getByDisplayValue('myorg/repo')).toBeInTheDocument();
    expect(screen.getByDisplayValue('main')).toBeInTheDocument();
    expect(screen.getByText('Feature branches + PRs')).toBeInTheDocument();
  });

  it('should call onSave with flat payload when Tracks a branch form is submitted', async () => {
    defaultProps.onSave.mockResolvedValue({});

    render(<GitSettingsModal {...defaultProps} />);

    await userEvent.click(screen.getByText('Git repo'));
    await userEvent.type(screen.getByLabelText(/Repository/), 'corp/defs');
    await userEvent.clear(screen.getByLabelText(/^Branch$/));
    await userEvent.type(screen.getByLabelText(/^Branch$/), 'develop');
    await userEvent.click(screen.getByRole('button', { name: /^Save$/ }));

    await waitFor(() => {
      expect(defaultProps.onSave).toHaveBeenCalledWith({
        github_repo_path: 'corp/defs',
        git_branch: 'develop',
      });
    });
  });

  it('should call onSave with root payload when Feature branches + PRs is selected', async () => {
    defaultProps.onSave.mockResolvedValue({});

    render(<GitSettingsModal {...defaultProps} />);

    await userEvent.click(screen.getByText('Git repo'));
    await userEvent.click(screen.getByText('Feature branches + PRs'));
    await userEvent.type(screen.getByLabelText(/Repository/), 'corp/defs');
    await userEvent.clear(screen.getByLabelText(/Default branch/));
    await userEvent.type(screen.getByLabelText(/Default branch/), 'trunk');
    await userEvent.click(screen.getByRole('button', { name: /^Save$/ }));

    await waitFor(() => {
      expect(defaultProps.onSave).toHaveBeenCalledWith({
        github_repo_path: 'corp/defs',
        default_branch: 'trunk',
      });
    });
  });

  it('should call onRemove (not onSave) when Edited in DataJunction is selected', async () => {
    defaultProps.onSave.mockResolvedValue({});
    defaultProps.onRemove.mockResolvedValue({});

    render(
      <GitSettingsModal
        {...defaultProps}
        currentConfig={{ github_repo_path: 'myorg/repo', git_branch: 'main' }}
      />,
    );

    // Source is already "git" from currentConfig; switch back to dj
    await userEvent.click(screen.getByText('Edited in DataJunction'));
    await userEvent.click(screen.getByRole('button', { name: /^Save$/ }));

    await waitFor(() => {
      expect(defaultProps.onRemove).toHaveBeenCalled();
    });
    expect(defaultProps.onSave).not.toHaveBeenCalled();
  });

  it('should show an error when Repository is empty and Git repo is selected', async () => {
    render(<GitSettingsModal {...defaultProps} />);

    await userEvent.click(screen.getByText('Git repo'));
    await userEvent.click(screen.getByRole('button', { name: /^Save$/ }));

    expect(screen.getByText('Repository is required')).toBeInTheDocument();
    expect(defaultProps.onSave).not.toHaveBeenCalled();
  });

  it('should show success message after save', async () => {
    defaultProps.onSave.mockResolvedValue({});

    render(<GitSettingsModal {...defaultProps} />);

    await userEvent.click(screen.getByText('Git repo'));
    await userEvent.type(screen.getByLabelText(/Repository/), 'myorg/repo');
    await userEvent.click(screen.getByRole('button', { name: /^Save$/ }));

    await waitFor(() => {
      expect(
        screen.getByText('Git configuration saved successfully!'),
      ).toBeInTheDocument();
    });
  });

  it('should show error when onSave returns _error response', async () => {
    defaultProps.onSave.mockResolvedValue({
      _error: true,
      message: 'Invalid repository',
    });

    render(<GitSettingsModal {...defaultProps} />);

    await userEvent.click(screen.getByText('Git repo'));
    await userEvent.type(screen.getByLabelText(/Repository/), 'invalid');
    await userEvent.click(screen.getByRole('button', { name: /^Save$/ }));

    await waitFor(() => {
      expect(screen.getByText('Invalid repository')).toBeInTheDocument();
    });
  });

  it('should show error when onSave throws exception', async () => {
    defaultProps.onSave.mockRejectedValue(new Error('Network error'));

    render(<GitSettingsModal {...defaultProps} />);

    await userEvent.click(screen.getByText('Git repo'));
    await userEvent.type(screen.getByLabelText(/Repository/), 'myorg/repo');
    await userEvent.click(screen.getByRole('button', { name: /^Save$/ }));

    await waitFor(() => {
      expect(screen.getByText('Network error')).toBeInTheDocument();
    });
  });

  it('should show default error message when onSave throws without message', async () => {
    defaultProps.onSave.mockRejectedValue({});

    render(<GitSettingsModal {...defaultProps} />);

    await userEvent.click(screen.getByText('Git repo'));
    await userEvent.type(screen.getByLabelText(/Repository/), 'myorg/repo');
    await userEvent.click(screen.getByRole('button', { name: /^Save$/ }));

    await waitFor(() => {
      expect(
        screen.getByText('Failed to save git settings'),
      ).toBeInTheDocument();
    });
  });

  it('should warn when switching a namespace with existing nodes to git', async () => {
    render(
      <GitSettingsModal {...defaultProps} currentConfig={null} nodeCount={3} />,
    );

    await userEvent.click(screen.getByText('Git repo'));

    expect(
      screen.getByText(/3 nodes here aren't in the repo/),
    ).toBeInTheDocument();
  });

  it('should call onClose when Cancel is clicked', async () => {
    render(<GitSettingsModal {...defaultProps} />);

    await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
    expect(defaultProps.onClose).toHaveBeenCalled();
  });

  it('should call onClose when clicking overlay', async () => {
    render(<GitSettingsModal {...defaultProps} />);

    const overlay = document.querySelector('.modal-overlay');
    fireEvent.click(overlay);
    expect(defaultProps.onClose).toHaveBeenCalled();
  });

  it('should call onClose when clicking close button', async () => {
    render(<GitSettingsModal {...defaultProps} />);

    await userEvent.click(screen.getByTitle('Close'));
    expect(defaultProps.onClose).toHaveBeenCalled();
  });

  it('should show Saving... and disable the button while saving', async () => {
    let resolveSave;
    defaultProps.onSave.mockImplementation(
      () =>
        new Promise(resolve => {
          resolveSave = resolve;
        }),
    );

    render(<GitSettingsModal {...defaultProps} />);

    await userEvent.click(screen.getByText('Git repo'));
    await userEvent.type(screen.getByLabelText(/Repository/), 'myorg/repo');
    await userEvent.click(screen.getByRole('button', { name: /^Save$/ }));

    expect(screen.getByText('Saving...')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: 'Saving...' })).toBeDisabled();

    resolveSave({});
  });

  it('should show Close button after success and call onClose when clicked', async () => {
    defaultProps.onSave.mockResolvedValue({});

    render(<GitSettingsModal {...defaultProps} />);

    await userEvent.click(screen.getByText('Git repo'));
    await userEvent.type(screen.getByLabelText(/Repository/), 'myorg/repo');
    await userEvent.click(screen.getByRole('button', { name: /^Save$/ }));

    await waitFor(() => {
      expect(
        screen.getByText('Git configuration saved successfully!'),
      ).toBeInTheDocument();
    });

    // Button changes from "Cancel" to "Close" after success
    const closeBtn = screen.getByRole('button', { name: 'Close' });
    await userEvent.click(closeBtn);
    expect(defaultProps.onClose).toHaveBeenCalled();
  });
});

describe('<CreatePRModal />', () => {
  const defaultProps = {
    isOpen: true,
    onClose: vi.fn(),
    onCreate: vi.fn(),
    namespace: 'analytics.feature_xyz',
    gitBranch: 'feature-xyz',
    parentBranch: 'main',
    repoPath: 'myorg/dj-definitions',
  };

  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should not render when isOpen is false', () => {
    render(<CreatePRModal {...defaultProps} isOpen={false} />);
    expect(screen.queryByText('Create Pull Request')).not.toBeInTheDocument();
  });

  it('should render the modal with branch flow', () => {
    render(<CreatePRModal {...defaultProps} />);

    expect(screen.getByText('Create Pull Request')).toBeInTheDocument();
    expect(screen.getByText('feature-xyz')).toBeInTheDocument();
    expect(screen.getByText('main')).toBeInTheDocument();
  });

  it('should disable submit when title is empty', () => {
    render(<CreatePRModal {...defaultProps} />);

    const submitButton = screen.getByRole('button', { name: 'Create PR' });
    expect(submitButton).toBeDisabled();
  });

  it('should disable submit when title is whitespace only', async () => {
    render(<CreatePRModal {...defaultProps} />);

    await userEvent.type(screen.getByLabelText(/Title/), '   ');

    const submitButton = screen.getByRole('button', { name: 'Create PR' });
    expect(submitButton).toBeDisabled();
  });

  it('should enable submit when title is entered', async () => {
    render(<CreatePRModal {...defaultProps} />);

    await userEvent.type(screen.getByLabelText(/Title/), 'My PR title');

    const submitButton = screen.getByRole('button', { name: 'Create PR' });
    expect(submitButton).not.toBeDisabled();
  });

  it('should call onCreate with title and body', async () => {
    defaultProps.onCreate.mockResolvedValue({
      pr_number: 42,
      pr_url: 'https://github.com/myorg/repo/pull/42',
      head_branch: 'feature-xyz',
      base_branch: 'main',
    });

    render(<CreatePRModal {...defaultProps} />);

    await userEvent.type(screen.getByLabelText(/Title/), 'Add new metrics');
    await userEvent.type(
      screen.getByLabelText(/Description/),
      'This PR adds...',
    );
    await userEvent.click(screen.getByRole('button', { name: 'Create PR' }));

    await waitFor(() => {
      expect(defaultProps.onCreate).toHaveBeenCalledWith(
        'Add new metrics',
        'This PR adds...',
        expect.any(Function), // progress callback
      );
    });
  });

  it('should call onCreate with null body if description is empty', async () => {
    defaultProps.onCreate.mockResolvedValue({
      pr_number: 42,
      pr_url: 'https://github.com/myorg/repo/pull/42',
      head_branch: 'feature-xyz',
      base_branch: 'main',
    });

    render(<CreatePRModal {...defaultProps} />);

    await userEvent.type(screen.getByLabelText(/Title/), 'Add new metrics');
    // Don't fill in description
    await userEvent.click(screen.getByRole('button', { name: 'Create PR' }));

    await waitFor(() => {
      expect(defaultProps.onCreate).toHaveBeenCalledWith(
        'Add new metrics',
        null,
        expect.any(Function),
      );
    });
  });

  it('should show success view after PR creation', async () => {
    defaultProps.onCreate.mockResolvedValue({
      pr_number: 42,
      pr_url: 'https://github.com/myorg/repo/pull/42',
      head_branch: 'feature-xyz',
      base_branch: 'main',
    });

    render(<CreatePRModal {...defaultProps} />);

    await userEvent.type(screen.getByLabelText(/Title/), 'Add new metrics');
    await userEvent.click(screen.getByRole('button', { name: 'Create PR' }));

    await waitFor(() => {
      expect(screen.getByText('Pull Request #42 Created!')).toBeInTheDocument();
    });
    expect(
      screen.getByRole('link', { name: 'View on GitHub' }),
    ).toHaveAttribute('href', 'https://github.com/myorg/repo/pull/42');
  });

  it('should show error when PR creation fails', async () => {
    defaultProps.onCreate.mockResolvedValue({
      _error: true,
      message: 'A PR already exists for this branch',
    });

    render(<CreatePRModal {...defaultProps} />);

    await userEvent.type(screen.getByLabelText(/Title/), 'Add new metrics');
    await userEvent.click(screen.getByRole('button', { name: 'Create PR' }));

    await waitFor(() => {
      expect(
        screen.getByText('A PR already exists for this branch'),
      ).toBeInTheDocument();
    });
  });

  it('should show error when onCreate throws exception', async () => {
    defaultProps.onCreate.mockRejectedValue(new Error('GitHub API error'));

    render(<CreatePRModal {...defaultProps} />);

    await userEvent.type(screen.getByLabelText(/Title/), 'Add new metrics');
    await userEvent.click(screen.getByRole('button', { name: 'Create PR' }));

    await waitFor(() => {
      expect(screen.getByText('GitHub API error')).toBeInTheDocument();
    });
  });

  it('should show default error when onCreate throws without message', async () => {
    defaultProps.onCreate.mockRejectedValue({});

    render(<CreatePRModal {...defaultProps} />);

    await userEvent.type(screen.getByLabelText(/Title/), 'Add new metrics');
    await userEvent.click(screen.getByRole('button', { name: 'Create PR' }));

    await waitFor(() => {
      expect(
        screen.getByText('Failed to create pull request'),
      ).toBeInTheDocument();
    });
  });

  it('should show progress states', async () => {
    let resolvePromise;
    defaultProps.onCreate.mockImplementation((title, body, onProgress) => {
      return new Promise(resolve => {
        resolvePromise = resolve;
        // Simulate progress
        setTimeout(() => onProgress('syncing'), 10);
        setTimeout(() => onProgress('creating'), 50);
      });
    });

    render(<CreatePRModal {...defaultProps} />);

    await userEvent.type(screen.getByLabelText(/Title/), 'Test');
    await userEvent.click(screen.getByRole('button', { name: 'Create PR' }));

    await waitFor(() => {
      expect(screen.getByText('Syncing to git...')).toBeInTheDocument();
    });

    // Resolve the promise to prevent test timeout
    resolvePromise({
      pr_number: 1,
      pr_url: 'https://github.com/test',
      head_branch: 'test',
      base_branch: 'main',
    });
  });

  it('should show Creating PR... progress state', async () => {
    let resolvePromise;
    defaultProps.onCreate.mockImplementation((title, body, onProgress) => {
      return new Promise(resolve => {
        resolvePromise = resolve;
        // Skip syncing, go straight to creating
        onProgress('creating');
      });
    });

    render(<CreatePRModal {...defaultProps} />);

    await userEvent.type(screen.getByLabelText(/Title/), 'Test');
    await userEvent.click(screen.getByRole('button', { name: 'Create PR' }));

    await waitFor(() => {
      expect(screen.getByText('Creating PR...')).toBeInTheDocument();
    });

    // Resolve the promise to prevent test timeout
    resolvePromise({
      pr_number: 1,
      pr_url: 'https://github.com/test',
      head_branch: 'test',
      base_branch: 'main',
    });
  });

  it('should call onClose when Cancel is clicked', async () => {
    render(<CreatePRModal {...defaultProps} />);

    await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
    expect(defaultProps.onClose).toHaveBeenCalled();
  });

  it('should call onClose when clicking overlay', async () => {
    render(<CreatePRModal {...defaultProps} />);

    const overlay = document.querySelector('.modal-overlay');
    fireEvent.click(overlay);
    expect(defaultProps.onClose).toHaveBeenCalled();
  });

  it('should call onClose when clicking close button', async () => {
    render(<CreatePRModal {...defaultProps} />);

    await userEvent.click(screen.getByTitle('Close'));
    expect(defaultProps.onClose).toHaveBeenCalled();
  });

  it('should reset state when closed from success view', async () => {
    defaultProps.onCreate.mockResolvedValue({
      pr_number: 42,
      pr_url: 'https://github.com/myorg/repo/pull/42',
      head_branch: 'feature-xyz',
      base_branch: 'main',
    });

    render(<CreatePRModal {...defaultProps} />);

    await userEvent.type(screen.getByLabelText(/Title/), 'Add new metrics');
    await userEvent.click(screen.getByRole('button', { name: 'Create PR' }));

    await waitFor(() => {
      expect(screen.getByText('Pull Request #42 Created!')).toBeInTheDocument();
    });

    // Click Close button in success view
    await userEvent.click(screen.getByRole('button', { name: 'Close' }));
    expect(defaultProps.onClose).toHaveBeenCalled();
  });

  it('should display branch flow info in success view', async () => {
    defaultProps.onCreate.mockResolvedValue({
      pr_number: 42,
      pr_url: 'https://github.com/myorg/repo/pull/42',
      head_branch: 'feature-xyz',
      base_branch: 'main',
    });

    render(<CreatePRModal {...defaultProps} />);

    await userEvent.type(screen.getByLabelText(/Title/), 'Add new metrics');
    await userEvent.click(screen.getByRole('button', { name: 'Create PR' }));

    await waitFor(() => {
      expect(screen.getByText('Pull Request #42 Created!')).toBeInTheDocument();
    });

    // Success view should show branch flow (text is combined like "feature-xyz → main")
    expect(screen.getByText(/feature-xyz.*→.*main/)).toBeInTheDocument();
  });
});
