import { HttpResponse, delay, http } from 'msw';
import { act, render, screen, waitFor } from 'test/test-utils';

import { PROVISIONING_API_BASE as BASE } from '@grafana/test-utils/handlers';
import server from '@grafana/test-utils/server';
import { setTestFlags } from '@grafana/test-utils/unstable';
import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv';
import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam';
import { type FolderDTO } from 'app/types/folders';

import {
  type ProvisionedFolderFormDataResult,
  useProvisionedFolderFormData,
} from '../../hooks/useProvisionedFolderFormData';
import { setupProvisioningMswServer } from '../../mocks/server';

import { NewProvisionedFolderForm } from './NewProvisionedFolderForm';

setupProvisioningMswServer();

jest.mock('@grafana/runtime', () => {
  const actual = jest.requireActual('@grafana/runtime');
  return {
    ...actual,
    config: {
      ...actual.config,
    },
  };
});

jest.mock('app/features/manage-dashboards/services/ValidationSrv', () => ({
  validationSrv: {
    validateNewFolderName: jest.fn(),
  },
}));
jest.mock('../../hooks/usePRBranch', () => ({
  usePRBranch: jest.fn().mockReturnValue(undefined),
}));

jest.mock('../../hooks/useLastBranch', () => ({
  useLastBranch: jest.fn().mockReturnValue({
    getLastBranch: jest.fn().mockReturnValue(undefined),
    setLastBranch: jest.fn(),
  }),
}));

jest.mock('../../hooks/useGetRepositoryFolders', () => ({
  useGetRepositoryFolders: jest.fn().mockReturnValue({ options: [], loading: false, error: null }),
}));

jest.mock('../../hooks/useProvisionedFolderFormData', () => ({
  useProvisionedFolderFormData: jest.fn(),
}));

jest.mock('app/features/provisioning/hooks/usePullRequestParam', () => ({
  usePullRequestParam: jest.fn(),
}));
jest.mock('react-router-dom-v5-compat', () => {
  const actual = jest.requireActual('react-router-dom-v5-compat');
  return {
    ...actual,
    useNavigate: () => jest.fn(),
  };
});

interface Props {
  onDismiss?: () => void;
  parentFolder?: FolderDTO;
}

function setup(props: Partial<Props> = {}, hookData = mockHookData) {
  const defaultProps: Props = {
    onDismiss: jest.fn(),
    parentFolder: {
      id: 1,
      uid: 'folder-uid',
      title: 'Parent Folder',
      url: '/dashboards/f/folder-uid',
      hasAcl: false,
      canSave: true,
      canEdit: true,
      canAdmin: true,
      canDelete: true,
      repository: {
        name: 'test-repo',
        type: 'github',
      },
    } as unknown as FolderDTO,
    ...props,
  };

  (useProvisionedFolderFormData as jest.Mock).mockReturnValue(hookData);

  return {
    ...render(<NewProvisionedFolderForm {...defaultProps} />),
    props: defaultProps,
  };
}

const mockHookData: ProvisionedFolderFormDataResult = {
  repository: {
    name: 'test-repo',
    title: 'Test Repository',
    type: 'github',
    workflows: ['write', 'branch'],
    target: 'folder',
  },
  isReadOnlyRepo: false,
  folder: {
    metadata: {
      annotations: {
        'grafana.app/sourcePath': 'dashboards',
      },
    },
    spec: {
      title: '',
    },
  },
  canPushToConfiguredBranch: true,
  initialValues: {
    title: '',
    comment: '',
    ref: 'folder/test-timestamp',
    repo: 'test-repo',
    path: '/dashboards',
    workflow: 'write',
  },
  isLoading: false,
  isMissingRepo: false,
};

function requireCapturedRequest(capturedRequest: { url: URL; body: unknown } | null): { url: URL; body: unknown } {
  expect(capturedRequest).not.toBeNull();
  return capturedRequest as { url: URL; body: unknown };
}

describe('NewProvisionedFolderForm', () => {
  let capturedRequest: { url: URL; body: unknown } | null = null;

  beforeEach(() => {
    capturedRequest = null;
    jest.clearAllMocks();
    (usePullRequestParam as jest.Mock).mockReturnValue({});
    (validationSrv.validateNewFolderName as jest.Mock).mockResolvedValue(true);
  });

  it('should render the form with correct fields', async () => {
    setup();

    expect(await screen.findByRole('textbox', { name: /folder name/i })).toBeInTheDocument();
    expect(screen.getByRole('textbox', { name: /comment/i })).toBeInTheDocument();
    expect(screen.getByRole('combobox', { name: /branch/i })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /^create$/i })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
  });

  it('should show a spinner while repository data is loading', async () => {
    setup(
      {},
      {
        ...mockHookData,
        repository: undefined,
        initialValues: undefined,
        isLoading: true,
      }
    );
    expect(await screen.findByTestId('Spinner')).toBeInTheDocument();
    expect(screen.queryByLabelText('Repository not found')).not.toBeInTheDocument();
  });

  it('should show error when repository is not found', async () => {
    setup(
      {},
      {
        ...mockHookData,
        repository: undefined,
        initialValues: undefined,
        isMissingRepo: true,
      }
    );
    expect(await screen.findByLabelText('Repository not found')).toBeInTheDocument();
  });

  it('should show branch field for git repositories', async () => {
    setup();

    expect(await screen.findByRole('combobox', { name: /branch/i })).toBeInTheDocument();
  });

  it('should validate branch name', async () => {
    const { user } = setup();

    const branchInput = await screen.findByRole('combobox', { name: /branch/i });
    await user.click(branchInput);
    await user.type(branchInput, 'invalid//branch');
    await user.keyboard('{Enter}');

    const submitButton = screen.getByRole('button', { name: /^create$/i });
    await user.click(submitButton);

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

  it('should create folder successfully', async () => {
    server.use(
      http.post(`${BASE}/repositories/:name/files/*`, async ({ request }) => {
        const url = new URL(request.url);
        capturedRequest = { url, body: await request.json() };
        return HttpResponse.json({
          resource: { upsert: { metadata: { name: 'new-folder' } } },
        });
      })
    );

    const { user, props } = setup();

    const folderNameInput = await screen.findByRole('textbox', { name: /folder name/i });
    const commentInput = screen.getByRole('textbox', { name: /comment/i });

    await user.clear(folderNameInput);
    await user.type(folderNameInput, 'New Test Folder');

    await user.clear(commentInput);
    await user.type(commentInput, 'Creating a new test folder');

    const submitButton = screen.getByRole('button', { name: /^create$/i });
    await user.click(submitButton);

    await waitFor(() => {
      expect(capturedRequest).not.toBeNull();
    });

    const request = requireCapturedRequest(capturedRequest);
    expect(request.url.pathname).toContain('/repositories/test-repo/files/');
    expect(request.url.pathname).toContain('New%20Test%20Folder');
    expect(request.url.searchParams.get('message')).toBe('Creating a new test folder');
    expect(request.body).toEqual({ title: 'New Test Folder', type: 'folder' });

    // The real request handler dismisses the form after a successful save
    await waitFor(() => {
      expect(props.onDismiss).toHaveBeenCalled();
    });
  });

  it('should not produce double slashes when folder annotation has trailing slash', async () => {
    server.use(
      http.post(`${BASE}/repositories/:name/files/*`, async ({ request }) => {
        const url = new URL(request.url);
        capturedRequest = { url, body: await request.json() };
        return HttpResponse.json({
          resource: { upsert: { metadata: { name: 'new-folder' } } },
        });
      })
    );

    const hookDataWithTrailingSlash = {
      ...mockHookData,
      folder: {
        metadata: {
          annotations: {
            'grafana.app/sourcePath': 'dashboards/',
          },
        },
        spec: {
          title: '',
        },
      },
    };

    const { user } = setup({}, hookDataWithTrailingSlash);

    const folderNameInput = await screen.findByRole('textbox', { name: /folder name/i });
    await user.clear(folderNameInput);
    await user.type(folderNameInput, 'New Folder');

    const submitButton = screen.getByRole('button', { name: /^create$/i });
    await user.click(submitButton);

    await waitFor(() => {
      expect(capturedRequest).not.toBeNull();
    });

    const request = requireCapturedRequest(capturedRequest);
    expect(request.url.pathname).not.toContain('//');
    expect(request.url.pathname).toContain('/dashboards/New%20Folder/');
  });

  it('should create folder with branch workflow', async () => {
    server.use(
      http.post(`${BASE}/repositories/:name/files/*`, async ({ request }) => {
        const url = new URL(request.url);
        capturedRequest = { url, body: await request.json() };
        return HttpResponse.json({
          resource: { upsert: { metadata: { name: 'new-folder' } } },
        });
      })
    );
    const { user } = setup(
      {},
      {
        ...mockHookData,
        initialValues: {
          ...mockHookData.initialValues!,
          ref: '',
          workflow: 'branch',
        },
      }
    );

    const folderNameInput = await screen.findByRole('textbox', { name: /folder name/i });
    await user.clear(folderNameInput);
    await user.type(folderNameInput, 'Branch Folder');

    const branchInput = screen.getByRole('combobox', { name: /branch/i });
    await user.click(branchInput);
    await user.clear(branchInput);
    await user.type(branchInput, 'feature/new-folder');
    await user.keyboard('{Enter}');

    const submitButton = screen.getByRole('button', { name: /^create$/i });
    await user.click(submitButton);

    await waitFor(() => {
      expect(capturedRequest).not.toBeNull();
    });

    const request = requireCapturedRequest(capturedRequest);
    expect(request.url.pathname).toContain('/repositories/test-repo/files/');
    expect(request.url.pathname).toContain('Branch%20Folder');
    expect(request.url.searchParams.get('ref')).toBe('feature/new-folder');
    expect(request.url.searchParams.get('message')).toBe('Create folder: Branch Folder');
    expect(request.body).toEqual({ title: 'Branch Folder', type: 'folder' });
  });

  it('renders the message from the repo commit template when comment is empty', async () => {
    server.use(
      http.post(`${BASE}/repositories/:name/files/*`, async ({ request }) => {
        const url = new URL(request.url);
        capturedRequest = { url, body: await request.json() };
        return HttpResponse.json({
          resource: { upsert: { metadata: { name: 'new-folder' } } },
        });
      })
    );
    const { user } = setup(
      {},
      {
        ...mockHookData,
        repository: {
          ...mockHookData.repository!,
          commit: { singleResourceMessageTemplate: 'chore({{resourceKind}}s): {{action}} {{title}}' },
        },
      }
    );

    const folderNameInput = await screen.findByRole('textbox', { name: /folder name/i });
    await user.clear(folderNameInput);
    await user.type(folderNameInput, 'Templated Folder');

    await user.click(screen.getByRole('button', { name: /^create$/i }));

    await waitFor(() => {
      expect(capturedRequest).not.toBeNull();
    });

    const request = requireCapturedRequest(capturedRequest);
    expect(request.url.searchParams.get('message')).toBe('chore(folders): create Templated Folder');
  });

  it('should send correct request body and show the error when folder creation fails', async () => {
    server.use(
      http.post(`${BASE}/repositories/:name/files/*`, async ({ request }) => {
        const url = new URL(request.url);
        capturedRequest = { url, body: await request.json() };
        return HttpResponse.json({ message: 'Failed to create folder' }, { status: 500 });
      })
    );

    const { user, props } = setup();

    const folderNameInput = await screen.findByRole('textbox', { name: /folder name/i });
    await user.clear(folderNameInput);
    await user.type(folderNameInput, 'Error Folder');

    const submitButton = screen.getByRole('button', { name: /^create$/i });
    await user.click(submitButton);

    await waitFor(() => {
      expect(capturedRequest).not.toBeNull();
    });

    const request = requireCapturedRequest(capturedRequest);
    expect(request.url.pathname).toContain('/repositories/test-repo/files/');
    expect(request.url.pathname).toContain('Error%20Folder');
    expect(request.url.searchParams.get('message')).toBe('Create folder: Error Folder');
    expect(request.body).toEqual({ title: 'Error Folder', type: 'folder' });

    // The form catches the error and surfaces it in an alert; it stays open
    expect(await screen.findByText('Failed to create folder')).toBeInTheDocument();
    expect(props.onDismiss).not.toHaveBeenCalled();
  });

  it('should disable create button when form is submitting', async () => {
    server.use(
      http.post(`${BASE}/repositories/:name/files/*`, async () => {
        await delay('infinite');
        return HttpResponse.json({});
      })
    );

    const { user } = setup();

    const folderNameInput = await screen.findByRole('textbox', { name: /folder name/i });
    await user.clear(folderNameInput);
    await user.type(folderNameInput, 'Test Folder');

    const submitButton = screen.getByRole('button', { name: /^create$/i });
    await user.click(submitButton);

    await waitFor(() => {
      expect(screen.getByRole('button', { name: /creating/i })).toBeDisabled();
    });
    expect(screen.getByRole('button', { name: /creating/i })).toHaveTextContent('Creating...');
  });

  it('should show PR link when PR URL is available', async () => {
    (usePullRequestParam as jest.Mock).mockReturnValue({ prURL: 'https://github.com/grafana/grafana/pull/1234' });

    setup();

    expect(await screen.findByText('Pull request created')).toBeInTheDocument();
    expect(screen.getByRole('link')).toHaveTextContent('https://github.com/grafana/grafana/pull/1234');
  });

  it('should call onDismiss when cancel button is clicked', async () => {
    const { user, props } = setup();

    const cancelButton = await screen.findByRole('button', { name: /cancel/i });
    await user.click(cancelButton);

    expect(props.onDismiss).toHaveBeenCalled();
  });

  it('should show read-only alert when repository has no workflows', async () => {
    setup(
      {},
      {
        ...mockHookData,
        repository: {
          name: 'test-repo',
          title: 'Test Repository',
          type: 'github',
          workflows: [],
          target: 'folder',
        },
      }
    );

    expect(await screen.findByText('This repository is read only')).toBeInTheDocument();
  });
});

describe('NewProvisionedFolderForm commit message template', () => {
  beforeEach(() => {
    setTestFlags({ 'provisioning.gitConventions': true });
  });

  afterEach(async () => {
    // setTestFlags fires OpenFeature events that update mounted components, so reset within act().
    await act(async () => {
      setTestFlags({});
    });
  });

  it('pre-fills Comment from the repository template', async () => {
    setup(
      {},
      {
        ...mockHookData,
        repository: {
          ...mockHookData.repository!,
          commit: { singleResourceMessageTemplate: 'feat({{resourceKind}}s): {{action}} {{title}}' },
        },
        initialValues: { ...mockHookData.initialValues!, title: 'Reports' },
      }
    );

    const comment = await screen.findByRole('textbox', { name: /comment/i });
    await waitFor(() => expect(comment).toHaveValue('feat(folders): create Reports'));
    expect(comment).not.toHaveAttribute('readonly');
  });
});
