import React from 'react';
import { readdirSync, readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { CRM_EMAIL_ALERT_RECIPIENT_TYPES } from '../src/approval-recipient-types';
import { ProcessActionsSection } from '../src/platform/features/approvals/components/ProcessActionsSection';
import { apiClient } from '../src/platform/infrastructure/api/client';
import { flowExecutionApi } from '../src/platform/infrastructure/api/flows/execution';
import type { Flow } from '../src/platform/infrastructure/api/flows/types';
import {
  clearEmailAlertRecipientTypesForTest,
  registerEmailAlertRecipientTypes,
} from '../src/platform/features/approvals/emailAlertRecipientTypeRegistry';
import { APPROVAL_EMAIL_NOTIFICATION_MARKER } from '../src/platform/features/approvals/utils/approvalEmailNotificationScaffold';

const frontendSourceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../src');

function readFrontendSources(directory: string): string {
  return readdirSync(directory, { withFileTypes: true })
    .filter(entry => entry.isDirectory() || /\.(ts|tsx)$/.test(entry.name))
    .map(entry => {
      const entryPath = path.join(directory, entry.name);
      return entry.isDirectory() ? readFrontendSources(entryPath) : readFileSync(entryPath, 'utf8');
    })
    .join('\n');
}

const apiMocks = vi.hoisted(() => ({
  createProcessAction: vi.fn(),
  deleteProcessAction: vi.fn(),
  updateApprovalProcess: vi.fn(),
  updateProcessAction: vi.fn(),
  getActiveUsers: vi.fn(),
  getUserRoles: vi.fn(),
  getGroups: vi.fn(),
  listApprovalNotificationFlows: vi.fn(),
  saveFlow: vi.fn(),
  activateVersion: vi.fn(),
  getById: vi.fn(),
  getTemplates: vi.fn(),
  getFolders: vi.fn(),
}));

vi.mock('../src/platform/infrastructure/api/approvals', () => ({
  approvalsAPI: {
    createProcessAction: apiMocks.createProcessAction,
    deleteProcessAction: apiMocks.deleteProcessAction,
    updateApprovalProcess: apiMocks.updateApprovalProcess,
    updateProcessAction: apiMocks.updateProcessAction,
    getActiveUsers: apiMocks.getActiveUsers,
    getUserRoles: apiMocks.getUserRoles,
    getGroups: apiMocks.getGroups,
  },
}));

vi.mock('../src/platform/infrastructure/api/flows', () => ({
  flowsApi: {
    listApprovalNotificationFlows: apiMocks.listApprovalNotificationFlows,
    saveFlow: apiMocks.saveFlow,
    activateVersion: apiMocks.activateVersion,
    getById: apiMocks.getById,
  },
}));

vi.mock('../src/platform/infrastructure/api/email-templates', () => ({
  emailTemplateAPI: {
    getTemplates: apiMocks.getTemplates,
    getFolders: apiMocks.getFolders,
  },
}));

describe('ProcessActionsSection email notification action sugar', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    clearEmailAlertRecipientTypesForTest();
    registerEmailAlertRecipientTypes(CRM_EMAIL_ALERT_RECIPIENT_TYPES);
    apiMocks.listApprovalNotificationFlows.mockResolvedValue([]);
    apiMocks.getActiveUsers.mockResolvedValue([]);
    apiMocks.getUserRoles.mockResolvedValue([]);
    apiMocks.getGroups.mockResolvedValue([]);
    apiMocks.createProcessAction.mockResolvedValue({ id: 'process-action-1' });
    apiMocks.getTemplates.mockResolvedValue([
      {
        Id: 'tmpl-submitted',
        Name: 'Approval Submitted',
        Subject: 'Submitted',
        FolderId: 'folder-approval',
        Folder: { Id: 'folder-approval', Name: 'Approval' },
        IsActive: true,
        CreatedDate: '2026-01-01T00:00:00Z',
        LastModifiedDate: '2026-01-01T00:00:00Z',
      },
      {
        Id: 'tmpl-inactive',
        Name: 'Inactive Template',
        Subject: 'Inactive',
        IsActive: false,
        CreatedDate: '2026-01-01T00:00:00Z',
        LastModifiedDate: '2026-01-01T00:00:00Z',
      },
    ]);
    apiMocks.getFolders.mockResolvedValue([
      { Id: 'folder-approval', Name: 'Approval', DeveloperName: 'Approval', Type: 'Email' },
    ]);
  });

  it('offers only metadata-staged action types and never mutates Flow metadata', async () => {
    apiMocks.listApprovalNotificationFlows.mockResolvedValue([
      {
        Id: 'flow-def-existing',
        Name: 'Existing Approval Flow',
        DeveloperName: 'Existing_Approval_Flow',
        Status: 'Active',
        TriggerObject: '',
        TriggerType: '',
        ActionType: 'AutoLaunchedFlow',
        FlowType: 'multistep',
        LastModifiedDate: '2026-01-01T00:00:00Z',
      },
    ]);
    renderActionsSection();

    fireEvent.click((await screen.findAllByRole('button', { name: /Add Action/i }))[0]);

    const actionType = screen.getByLabelText('Action Type') as HTMLSelectElement;
    expect(Array.from(actionType.options).map(option => option.textContent)).toEqual([
      'Field Update',
      'Flow',
    ]);
    expect(actionType).not.toHaveTextContent('Email Notification');

    fireEvent.change(screen.getByLabelText('Action Name'), { target: { value: 'Run Existing Flow' } });
    fireEvent.change(actionType, { target: { value: 'Flow' } });
    const flowSelect = await screen.findByLabelText('Flow');
    expect(flowSelect).toHaveTextContent('Existing Approval Flow');
    fireEvent.change(flowSelect, { target: { value: 'Existing_Approval_Flow' } });
    fireEvent.click(screen.getByRole('button', { name: 'Save Action' }));

    await waitFor(() => {
      expect(apiMocks.createProcessAction).toHaveBeenCalledWith(expect.objectContaining({
        Name: 'Run Existing Flow',
        ActionType: 'Flow',
        ApiName: 'Existing_Approval_Flow',
      }));
    });
    expect(apiMocks.saveFlow).not.toHaveBeenCalled();
    expect(apiMocks.activateVersion).not.toHaveBeenCalled();
    expect(apiMocks.getById).not.toHaveBeenCalled();
  });

  it('labels existing generated Flow actions as Email Notification actions after reload', async () => {
    apiMocks.listApprovalNotificationFlows.mockResolvedValue([
      {
        Id: 'flow-def-1',
        Name: 'Notify Finance',
        DeveloperName: 'Notify_Finance_1782967164242',
        Status: 'Active',
        TriggerObject: '',
        TriggerType: '',
        ActionType: 'AutoLaunchedFlow',
        FlowType: 'multistep',
        LastModifiedDate: '2026-01-01T00:00:00Z',
        Description: APPROVAL_EMAIL_NOTIFICATION_MARKER,
      },
    ]);
    renderActionsSection({
      actions: [
        {
          Id: 'process-action-1',
          Name: 'Notify Finance',
          ProcessDefinitionId: '04aPROC000000001',
          ProcessNodeId: null,
          ActionEvent: 'Submit',
          ActionType: 'Flow',
          ApiName: 'Notify_Finance_1782967164242',
          EmailTemplateId: 'tmpl-submitted',
          Order: 0,
        },
      ],
    });

    expect(await screen.findByText('Email Notification')).toBeInTheDocument();
    expect(await screen.findByText('Approval Submitted')).toBeInTheDocument();
    expect(screen.queryByText(/Flow:/)).not.toBeInTheDocument();
    expect(screen.queryByTitle('Edit action')).not.toBeInTheDocument();
  });

  it('keeps canonical generated Flow actions read-only without the legacy template payload arm', async () => {
    apiMocks.listApprovalNotificationFlows.mockResolvedValue([
      {
        Id: 'flow-def-1',
        Name: 'Notify Finance',
        DeveloperName: 'ApprovalEmail_Finance_Submit_Notify',
        Status: 'Active',
        TriggerObject: '',
        TriggerType: '',
        ActionType: 'AutoLaunchedFlow',
        FlowType: 'multistep',
        LastModifiedDate: '2026-01-01T00:00:00Z',
        Description: APPROVAL_EMAIL_NOTIFICATION_MARKER,
      },
    ]);
    renderActionsSection({
      actions: [
        {
          Id: 'process-action-1',
          Name: 'Notify Finance',
          ProcessDefinitionId: '04aPROC000000001',
          ProcessNodeId: null,
          ActionEvent: 'Submit',
          ActionType: 'Flow',
          ApiName: 'ApprovalEmail_Finance_Submit_Notify',
          Order: 0,
        },
      ],
    });

    expect(await screen.findByText('Email Notification')).toBeInTheDocument();
    expect(screen.queryByText(/Template:/)).not.toBeInTheDocument();
    expect(screen.queryByTitle('Edit action')).not.toBeInTheDocument();
    await waitFor(() => expect(apiMocks.listApprovalNotificationFlows).toHaveBeenCalled());
  });

  it('keeps metadata-marker-only Flow actions read-only before and after canonical reload', async () => {
    let resolveFlows!: (flows: Flow[]) => void;
    apiMocks.listApprovalNotificationFlows.mockReturnValue(new Promise<Flow[]>(resolve => {
      resolveFlows = resolve;
    }));
    renderActionsSection({
      actions: [
        {
          Id: 'process-action-1',
          Name: 'Notify Finance',
          ProcessDefinitionId: '04aPROC000000001',
          ProcessNodeId: null,
          ActionEvent: 'Submit',
          ActionType: 'Flow',
          ApiName: 'Custom_Approval_Notification',
          Order: 0,
        },
      ],
    });

    expect(screen.queryByTitle('Edit action')).not.toBeInTheDocument();
    await act(async () => {
      resolveFlows([
        {
          Id: 'flow-def-1',
          Name: 'Notify Finance',
          DeveloperName: 'Custom_Approval_Notification',
          Status: 'Active',
          TriggerObject: '',
          TriggerType: '',
          ActionType: 'AutoLaunchedFlow',
          FlowType: 'multistep',
          LastModifiedDate: '2026-01-01T00:00:00Z',
          Description: APPROVAL_EMAIL_NOTIFICATION_MARKER,
        },
      ]);
    });

    expect(await screen.findByText('Email Notification')).toBeInTheDocument();
    expect(screen.queryByTitle('Edit action')).not.toBeInTheDocument();
    expect(apiMocks.listApprovalNotificationFlows).toHaveBeenCalled();
  });

  it('keeps Flow actions non-editable when metadata lookup fails', async () => {
    let rejectFlows!: (error: Error) => void;
    apiMocks.listApprovalNotificationFlows.mockReturnValue(new Promise<Flow[]>((_, reject) => {
      rejectFlows = reject;
    }));
    renderActionsSection({
      actions: [
        {
          Id: 'process-action-1',
          Name: 'Notify Finance',
          ProcessDefinitionId: '04aPROC000000001',
          ProcessNodeId: null,
          ActionEvent: 'Submit',
          ActionType: 'Flow',
          ApiName: 'Custom_Approval_Notification',
          Order: 0,
        },
      ],
    });

    expect(screen.queryByTitle('Edit action')).not.toBeInTheDocument();
    await act(async () => {
      rejectFlows(new Error('lookup unavailable'));
    });
    expect(screen.queryByTitle('Edit action')).not.toBeInTheDocument();
  });

  it('keeps stale template markers from classifying arbitrary Flow actions as Email Notifications', async () => {
    apiMocks.updateProcessAction.mockResolvedValue(undefined);
    renderActionsSection({
      actions: [
        {
          Id: 'process-action-1',
          Name: 'Notify Finance',
          ProcessDefinitionId: '04aPROC000000001',
          ProcessNodeId: null,
          ActionEvent: 'Submit',
          ActionType: 'Flow',
          ApiName: 'Notify_Finance_1782967164242',
          EmailTemplateId: 'tmpl-submitted',
          Order: 0,
        },
      ],
    });

    expect(await screen.findByText('Flow')).toBeInTheDocument();
    expect(await screen.findByText(/Flow:/)).toBeInTheDocument();
    expect(screen.queryByText('Email Notification')).not.toBeInTheDocument();
    expect(screen.queryByText(/Template:/)).not.toBeInTheDocument();

    fireEvent.click(screen.getByTitle('Edit action'));
    const updateButton = screen.getByRole('button', { name: 'Update Action' });
    await waitFor(() => {
      expect(updateButton).not.toBeDisabled();
    });
    fireEvent.click(updateButton);

    await waitFor(() => {
      expect(apiMocks.updateProcessAction).toHaveBeenCalledWith('process-action-1', expect.objectContaining({
        ApiName: 'Notify_Finance_1782967164242',
        EmailTemplateId: null,
      }));
    });
  });

  it('keeps the detail presentation read-only and routes authoring to the metadata editor', async () => {
    const onRequestEdit = vi.fn();
    render(
      <ProcessActionsSection
        mode="readOnly"
        processDefinitionId="04aPROC000000001"
        actions={[{
          Id: 'process-action-1',
          Name: 'Set Status',
          ProcessDefinitionId: '04aPROC000000001',
          ProcessNodeId: null,
          ActionEvent: 'Submit',
          ActionType: 'FieldUpdate',
          FieldApiName: 'Status',
          FieldValue: 'Pending',
          Order: 0,
        }]}
        onRequestEdit={onRequestEdit}
      />,
    );

    expect(await screen.findByText('Set Status')).toBeInTheDocument();
    expect(screen.queryByRole('button', { name: /Add Action/i })).not.toBeInTheDocument();
    expect(screen.queryByTitle('Edit action')).not.toBeInTheDocument();
    expect(screen.queryByTitle('Delete action')).not.toBeInTheDocument();

    fireEvent.click(screen.getByRole('button', { name: 'Edit approval process' }));
    expect(onRequestEdit).toHaveBeenCalledTimes(1);
  });
});

describe('approval notification Flow lookup failure contract', () => {
  it('propagates lookup errors so the editor can fail closed', async () => {
    const failure = new Error('lookup unavailable');
    const getSpy = vi.spyOn(apiClient, 'get').mockRejectedValueOnce(failure);
    try {
      await expect(flowExecutionApi.listApprovalNotificationFlows()).rejects.toBe(failure);
    } finally {
      getSpy.mockRestore();
    }
  });
});

describe('approval child authoring source of truth', () => {
  it('has no immediate Flow mutation path in the staged approval action editor', () => {
    const source = readFileSync(
      path.join(frontendSourceRoot, 'platform/features/approvals/components/ProcessActionsSection.tsx'),
      'utf8',
    );

    expect(source).not.toMatch(/\b(?:saveFlow|activateVersion|getById)\b/);
  });

  it('has no direct ProcessAction mutation client in CRM frontend source', () => {
    const source = readFrontendSources(frontendSourceRoot);

    expect(source).not.toMatch(/createProcessAction|updateProcessAction|deleteProcessAction/);
    expect(source).not.toMatch(/RECORDS?\(['"]ProcessAction['"]/);
  });
});

function renderActionsSection(overrides: Partial<React.ComponentProps<typeof ProcessActionsSection>> = {}) {
  return render(
    <ProcessActionsSection
      mode="editable"
      processDefinitionId="04aPROC000000001"
      actions={[]}
      onActionsChanged={vi.fn()}
      onCreateAction={apiMocks.createProcessAction}
      onUpdateAction={(action, data) => apiMocks.updateProcessAction(action.Id, data)}
      onDeleteAction={(action) => apiMocks.deleteProcessAction(action.Id)}
      onUpdateLock={(field, value) => apiMocks.updateApprovalProcess('04aPROC000000001', { [field]: value })}
      fieldOptions={[]}
      schemaFields={[]}
      {...overrides}
    />,
  );
}
