import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
  createCrmActivityTimelineAdapters,
  type CrmActivityTimelineActionConfig,
  type CrmActivityTimelineObjectMetadata,
  type CrmActivityTimelineStandardAction,
} from '../src/activity-timeline-adapter';
import { createCrmRuntimeRendererAdapters } from '../src/runtime-renderer-adapters';
import type { CrmActivityQueryApi } from '../src/activity-timeline-data';
import type { CrmConvertLeadApi, CrmConvertLeadLookupProps } from '../src/convert-lead-modal';
import type { CrmCreateRecordFromSourceApi } from '../src/create-record-from-source-modal';
import type { CrmSendRecordEmailApi } from '../src/send-record-email-modal';
import type { CrmTaskActivityApi } from '../src/task-activity-quick-create-modal';

const noopConvertLeadApi: CrmConvertLeadApi = {
  query: vi.fn(),
  previewStandardAction: vi.fn(),
  executeStandardAction: vi.fn(),
  getSchema: vi.fn(),
  warn: vi.fn(),
};

const noopCreateRecordFromSourceApi: CrmCreateRecordFromSourceApi = {
  executeStandardAction: vi.fn(),
  previewStandardAction: vi.fn(),
};

const noopTaskActivityApi: CrmTaskActivityApi = {
  executeStandardAction: vi.fn(),
};

const noopSendRecordEmailApi: CrmSendRecordEmailApi = {
  getConnectedGmail: vi.fn(),
  getRecordEmailRecipient: vi.fn(),
  beginGmailConnection: vi.fn(),
  uploadAttachment: vi.fn(),
  executeStandardAction: vi.fn(),
};

const Lookup: React.FC<CrmConvertLeadLookupProps> = ({ placeholder }) => (
  <div>{placeholder}</div>
);

const TEST_ACTIVITY_RENDERER_ID = 'app-pack:crm/action-renderer/task-activity-quick-create';

interface TestActivityAction extends CrmActivityTimelineStandardAction {
  visibilityCondition?: string;
}

interface TestActivityActionConfig extends CrmActivityTimelineActionConfig {
  type?: string;
  label?: string;
}

const defaultActivityActions: TestActivityAction[] = [
  {
    name: 'taskCall',
    label: 'Log a Call',
    category: 'ActivityTimeline',
    rendererId: TEST_ACTIVITY_RENDERER_ID,
    targetObjects: ['Opportunity'],
  },
  {
    name: 'taskEmail',
    label: 'Email',
    category: 'ActivityTimeline',
    rendererId: TEST_ACTIVITY_RENDERER_ID,
    targetObjects: ['Lead'],
  },
  {
    name: 'convertLead',
    label: 'Convert Lead',
    category: 'LeadActions',
    rendererId: 'app-pack:crm/action-renderer/lead-convert',
    targetObjects: ['Lead'],
  },
];

function resolvePlacedStandardActionsForTest(
  quickActions: readonly TestActivityActionConfig[] | undefined,
  standardActions: readonly TestActivityAction[],
  objectApiName: string,
): readonly TestActivityAction[] {
  const placements = (quickActions ?? []).filter(action => {
    const type = typeof action.type === 'string' ? action.type.toLowerCase() : 'standard';
    return type === 'standard';
  });
  const resolved: TestActivityAction[] = [];
  const normalizedObject = objectApiName.toLowerCase();

  for (const placement of placements) {
    const requestedName = placement.name ?? placement.actionName;
    if (!requestedName) {
      return [];
    }
    const matches = standardActions.filter(
      action => action.name.toLowerCase() === requestedName.toLowerCase(),
    );
    if (matches.length !== 1) {
      return [];
    }
    const action = matches[0];
    const targets = action.targetObjects ?? [];
    if (
      targets.length > 0
      && !targets.some(target => target.toLowerCase() === normalizedObject)
    ) {
      return [];
    }
    resolved.push(action);
  }

  return resolved;
}

function filterVisibleActionsForRecordForTest(
  actions: readonly TestActivityAction[],
  record: Record<string, unknown> | null | undefined,
): readonly TestActivityAction[] {
  if (!record) {
    return actions;
  }

  return actions.filter(action => {
    if (!action.visibilityCondition) {
      return true;
    }
    if (action.visibilityCondition === 'record.Status == "Active"') {
      return record.Status === 'Active';
    }
    return false;
  });
}

function renderActivityTimelineAdapter({
  quickActions,
  quickActionsAuthored,
  hostRecord,
  standardActions = defaultActivityActions,
  objectApiName = 'Opportunity',
  objectMetadata = { name: objectApiName },
  activityQueryApi = {
    query: vi.fn().mockResolvedValue([]),
  },
  resolvePlacedStandardActions = resolvePlacedStandardActionsForTest,
  filterVisibleActionsForRecord = filterVisibleActionsForRecordForTest,
}: {
  quickActions?: readonly TestActivityActionConfig[] | null;
  quickActionsAuthored?: boolean;
  hostRecord?: Record<string, unknown> | null;
  standardActions?: readonly TestActivityAction[];
  objectApiName?: string;
  objectMetadata?: CrmActivityTimelineObjectMetadata;
  activityQueryApi?: CrmActivityQueryApi;
  resolvePlacedStandardActions?: (
    quickActions: readonly TestActivityActionConfig[] | undefined,
    standardActions: readonly TestActivityAction[],
    objectApiName: string,
  ) => readonly TestActivityAction[];
  filterVisibleActionsForRecord?: (
    actions: readonly TestActivityAction[],
    record: Record<string, unknown> | null | undefined,
  ) => readonly TestActivityAction[];
}) {
  const navigateToRecord = vi.fn();
  const ActionRenderer: React.FC<{
    record: Record<string, unknown>;
  }> = ({ record }) => (
    <div>task activity renderer opened for {String(record.Name)}</div>
  );

  const { ActivityTimeline } = createCrmActivityTimelineAdapters<
    TestActivityAction,
    TestActivityActionConfig,
    CrmActivityTimelineObjectMetadata,
    {
      recordId: string;
      record: Record<string, unknown>;
      objectMetadata: CrmActivityTimelineObjectMetadata;
    }
  >({
    activityQueryApi,
    useObjectMetadata: () => ({ metadata: objectMetadata }),
    useStandardActions: () => standardActions,
    getActionRenderer: rendererId => (rendererId === TEST_ACTIVITY_RENDERER_ID ? ActionRenderer : null),
    resolvePlacedStandardActions,
    filterVisibleActionsForRecord,
    useNavigation: () => ({
      navigateToRecord,
      navigateToNewRecord: vi.fn(),
    }),
  });

  render(
    <ActivityTimeline
      recordId="006000000000001"
      objectApiName={objectApiName}
      recordName="Big Deal"
      quickActions={quickActions}
      quickActionsAuthored={quickActionsAuthored}
      hostRecord={hostRecord}
    />,
  );

  return {
    activityQueryApi,
    navigateToRecord,
  };
}

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

describe('CRM frontend adapter factories', () => {
  it('creates runtime renderer adapters from host-provided services', async () => {
    const navigateToRecord = vi.fn();
    const { ConvertedLeadBanner } = createCrmRuntimeRendererAdapters({
      convertLeadApi: noopConvertLeadApi,
      createRecordFromSourceApi: noopCreateRecordFromSourceApi,
      taskActivityApi: noopTaskActivityApi,
      sendRecordEmailApi: noopSendRecordEmailApi,
      lookupComponent: Lookup,
      useTaskObjectMetadata: () => ({ metadata: { name: 'Task', fields: [] } }),
      useCurrentUserId: () => '005000000000001',
    });

    render(
      <ConvertedLeadBanner
        record={{ ConvertedAccountId: '001ACCT001' }}
        host={{ navigateToRecord }}
      />,
    );

    await fireEvent.click(screen.getByRole('button', { name: /view account/i }));
    expect(navigateToRecord).toHaveBeenCalledWith('Account', '001ACCT001');
  });

  it('creates an activity timeline adapter without importing Nexus runtime modules', async () => {
    const activityQueryApi: CrmActivityQueryApi = {
      query: vi.fn().mockResolvedValue([]),
    };
    const navigateToRecord = vi.fn();
    const ActionRenderer: React.FC<{
      record: Record<string, unknown>;
    }> = ({ record }) => (
      <div>renderer relation: {String(record._ActivityTimelineRelationField)}</div>
    );
    const standardActions: CrmActivityTimelineStandardAction[] = [{
      name: 'taskCall',
      label: 'Log a Call',
      category: 'ActivityTimeline',
      rendererId: 'app-pack:crm/action-renderer/task-activity-quick-create',
      targetObjects: ['Lead'],
    }];

    const { ActivityTimeline } = createCrmActivityTimelineAdapters<
      CrmActivityTimelineStandardAction,
      { name: string },
      CrmActivityTimelineObjectMetadata,
      {
        recordId: string;
        record: Record<string, unknown>;
        objectMetadata: CrmActivityTimelineObjectMetadata;
      }
    >({
      activityQueryApi,
      useObjectMetadata: () => ({ metadata: { name: 'Lead' } }),
      useStandardActions: () => standardActions,
      getActionRenderer: () => ActionRenderer,
      resolvePlacedStandardActions: (_quickActions, actions) => actions,
      filterVisibleActionsForRecord: (actions) => actions,
      useNavigation: () => ({
        navigateToRecord,
        navigateToNewRecord: vi.fn(),
      }),
    });

    render(
      <ActivityTimeline
        recordId="003000000000001"
        objectApiName="Lead"
        recordName="Ada Lovelace"
      />,
    );

    await waitFor(() => {
      expect(activityQueryApi.query).toHaveBeenCalledWith(expect.objectContaining({
        objectApiName: 'Task',
        filterExpr: "WhoId = '003000000000001'",
      }));
    });

    fireEvent.click(screen.getByRole('button', { name: 'Log a Call' }));
    expect(screen.getByText('renderer relation: WhoId')).toBeInTheDocument();
  });

  it('renders authored activity quick actions for the current object and opens the registered renderer', async () => {
    renderActivityTimelineAdapter({
      quickActions: [
        {
          name: 'taskCall',
          label: 'Log a Call',
          type: 'Standard',
        },
        {
          name: 'nonStandardPlacement',
          label: 'Ignored',
          type: 'CreateRecord',
        },
      ],
    });

    await waitFor(() => {
      expect(screen.getByRole('button', { name: /log a call/i })).toBeInTheDocument();
    });

    expect(screen.queryByRole('button', { name: /^email$/i })).not.toBeInTheDocument();
    expect(screen.queryByRole('button', { name: /convert lead/i })).not.toBeInTheDocument();

    await userEvent.click(screen.getByRole('button', { name: /log a call/i }));
    expect(screen.getByText('task activity renderer opened for Big Deal')).toBeInTheDocument();
  });

  it('falls back to legacy activity placement when layout quick actions are not authored yet', async () => {
    renderActivityTimelineAdapter({});

    await waitFor(() => {
      expect(screen.getByRole('button', { name: /log a call/i })).toBeInTheDocument();
    });

    expect(screen.queryByRole('button', { name: /^email$/i })).not.toBeInTheDocument();
    expect(screen.queryByRole('button', { name: /convert lead/i })).not.toBeInTheDocument();
  });

  it('falls back to legacy activity placement for default empty quick action arrays', async () => {
    renderActivityTimelineAdapter({
      quickActions: [],
    });

    await waitFor(() => {
      expect(screen.getByRole('button', { name: /log a call/i })).toBeInTheDocument();
    });

    expect(screen.queryByRole('button', { name: /^email$/i })).not.toBeInTheDocument();
    expect(screen.queryByRole('button', { name: /convert lead/i })).not.toBeInTheDocument();
  });

  it('does not fall back to legacy placement when authored quick actions filter to empty', async () => {
    const resolvePlacedStandardActions = vi.fn(resolvePlacedStandardActionsForTest);

    renderActivityTimelineAdapter({
      quickActions: [],
      quickActionsAuthored: true,
      resolvePlacedStandardActions,
    });

    await waitFor(() => {
      expect(resolvePlacedStandardActions).toHaveBeenCalledWith(
        [],
        defaultActivityActions,
        'Opportunity',
      );
    });

    expect(screen.queryByRole('button', { name: /log a call/i })).not.toBeInTheDocument();
    expect(screen.queryByRole('button', { name: /^email$/i })).not.toBeInTheDocument();
    expect(screen.queryByRole('button', { name: /convert lead/i })).not.toBeInTheDocument();
  });

  it('fails closed when layout quick action names do not resolve to a unique standard action', async () => {
    renderActivityTimelineAdapter({
      standardActions: [
        {
          name: 'taskCall',
          label: 'Log a Call',
          rendererId: TEST_ACTIVITY_RENDERER_ID,
        },
        {
          name: 'taskCall',
          label: 'Duplicate Log a Call',
          rendererId: TEST_ACTIVITY_RENDERER_ID,
        },
      ],
      quickActions: [
        {
          name: 'taskCall',
          label: 'Log a Call',
          type: 'Standard',
        },
        {
          name: 'missingAction',
          label: 'Missing Action',
          type: 'Standard',
        },
      ],
    });

    await waitFor(() => {
      expect(screen.getByText(/no activities found/i)).toBeInTheDocument();
    });

    expect(screen.queryByRole('button', { name: /log a call/i })).not.toBeInTheDocument();
    expect(screen.queryByRole('button', { name: /missing action/i })).not.toBeInTheDocument();
  });

  it('fails closed when the layout references an action that does not apply to the current object', async () => {
    renderActivityTimelineAdapter({
      quickActions: [
        {
          name: 'taskEmail',
          label: 'Email',
          type: 'Standard',
        },
      ],
    });

    await waitFor(() => {
      expect(screen.getByText(/no activities found/i)).toBeInTheDocument();
    });

    expect(screen.queryByRole('button', { name: /^email$/i })).not.toBeInTheDocument();
  });

  it('filters authored activity actions with the host record visibility context', async () => {
    const actionsWithVisibility: TestActivityAction[] = [
      {
        name: 'taskCall',
        label: 'Log a Call',
        category: 'ActivityTimeline',
        rendererId: TEST_ACTIVITY_RENDERER_ID,
        targetObjects: ['Opportunity'],
        visibilityCondition: 'record.Status == "Active"',
      },
    ];

    renderActivityTimelineAdapter({
      standardActions: actionsWithVisibility,
      quickActions: [
        {
          name: 'taskCall',
          label: 'Log a Call',
          type: 'Standard',
        },
      ],
      hostRecord: { Status: 'Active' },
    });

    await waitFor(() => {
      expect(screen.getByRole('button', { name: /log a call/i })).toBeInTheDocument();
    });
  });

  it('hides authored activity actions when the host record fails visibility', async () => {
    const actionsWithVisibility: TestActivityAction[] = [
      {
        name: 'taskCall',
        label: 'Log a Call',
        category: 'ActivityTimeline',
        rendererId: TEST_ACTIVITY_RENDERER_ID,
        targetObjects: ['Opportunity'],
        visibilityCondition: 'record.Status == "Active"',
      },
    ];
    const filterVisibleActionsForRecord = vi.fn(filterVisibleActionsForRecordForTest);

    renderActivityTimelineAdapter({
      standardActions: actionsWithVisibility,
      quickActions: [
        {
          name: 'taskCall',
          label: 'Log a Call',
          type: 'Standard',
        },
      ],
      hostRecord: { Status: 'Closed' },
      filterVisibleActionsForRecord,
    });

    await waitFor(() => {
      expect(filterVisibleActionsForRecord).toHaveBeenCalledWith(
        actionsWithVisibility,
        { Status: 'Closed' },
      );
    });

    expect(screen.queryByRole('button', { name: /log a call/i })).not.toBeInTheDocument();
  });

  it('defaults activity actions to visible when no host record is supplied', async () => {
    renderActivityTimelineAdapter({
      standardActions: [
        {
          name: 'taskCall',
          label: 'Log a Call',
          category: 'ActivityTimeline',
          rendererId: TEST_ACTIVITY_RENDERER_ID,
          targetObjects: ['Opportunity'],
          visibilityCondition: 'record.Status == "Active"',
        },
      ],
      quickActions: [
        {
          name: 'taskCall',
          label: 'Log a Call',
          type: 'Standard',
        },
      ],
    });

    await waitFor(() => {
      expect(screen.getByRole('button', { name: /log a call/i })).toBeInTheDocument();
    });
  });

  it('opens the backing EmailMessage for email task rows when EmailMessage is available', async () => {
    const queryAll = vi.fn<NonNullable<CrmActivityQueryApi['queryAll']>>().mockResolvedValue([{
      ActivityId: '00T000000000001AAA',
      emailMessageId: '02s000000000001AAA',
    }]);
    const query = vi.fn<CrmActivityQueryApi['query']>().mockImplementation(async request => {
      if (request.objectApiName === 'Task') {
        return [{
          Id: '00T000000000001AAA',
          Subject: 'Email: quarterly sync',
          ActivityDate: '2026-05-21',
          Type: 'Email',
          Status: 'Completed',
          IsClosed: true,
        }];
      }
      if (request.objectApiName === 'EmailMessage') {
        return [{
          Id: '02s000000000001AAA', ActivityId: '00T000000000001AAA',
          Subject: 'Quarterly sync — composed subject',
        }];
      }
      return [];
    });
    const getSchema = vi.fn().mockResolvedValue({ schema: { name: 'EmailMessage' } });
    const { navigateToRecord } = renderActivityTimelineAdapter({
      objectApiName: 'Lead',
      activityQueryApi: { query, queryAll, getSchema } as CrmActivityQueryApi & {
        getSchema: typeof getSchema;
      },
    });

    await userEvent.click(await screen.findByText('Quarterly sync — composed subject'));

    await waitFor(() => {
      expect(navigateToRecord).toHaveBeenCalledWith('EmailMessage', '02s000000000001AAA');
    });
    expect(getSchema).toHaveBeenCalledWith('EmailMessage');
    expect(query).toHaveBeenCalledWith(expect.objectContaining({
      objectApiName: 'EmailMessage',
      selectFields: ['Id', 'ActivityId', 'Subject'],
      filterExpr: "Id IN ('02s000000000001AAA')",
      limit: 1,
    }));
    expect(navigateToRecord).not.toHaveBeenCalledWith('Task', '00T000000000001AAA');
  });

  it('falls back to Task detail when an email task has no linked EmailMessage', async () => {
    const query = vi.fn<CrmActivityQueryApi['query']>().mockImplementation(async request => {
      if (request.objectApiName === 'Task') {
        return [{
          Id: '00T000000000001AAA',
          Subject: 'Email: quarterly sync',
          ActivityDate: '2026-05-21',
          Type: 'Email',
          Status: 'Completed',
          IsClosed: true,
        }];
      }
      return [];
    });
    const getSchema = vi.fn().mockResolvedValue({ schema: { name: 'EmailMessage' } });
    const { navigateToRecord } = renderActivityTimelineAdapter({
      objectApiName: 'Lead',
      activityQueryApi: { query, getSchema } as CrmActivityQueryApi & {
        getSchema: typeof getSchema;
      },
    });

    await userEvent.click(await screen.findByText('Email: quarterly sync'));

    await waitFor(() => {
      expect(navigateToRecord).toHaveBeenCalledWith('Task', '00T000000000001AAA');
    });
  });

  it('falls back to Task detail without querying EmailMessage when EmailMessage schema is unavailable', async () => {
    const query = vi.fn<CrmActivityQueryApi['query']>().mockImplementation(async request => {
      if (request.objectApiName === 'Task') {
        return [{
          Id: '00T000000000001AAA',
          Subject: 'Email: quarterly sync',
          ActivityDate: '2026-05-21',
          Type: 'Email',
          Status: 'Completed',
          IsClosed: true,
        }];
      }
      return [];
    });
    const getSchema = vi.fn().mockRejectedValue(new Error('Object not found'));
    const { navigateToRecord } = renderActivityTimelineAdapter({
      objectApiName: 'Lead',
      activityQueryApi: { query, getSchema } as CrmActivityQueryApi & {
        getSchema: typeof getSchema;
      },
    });

    await userEvent.click(await screen.findByText('Email: quarterly sync'));

    await waitFor(() => {
      expect(navigateToRecord).toHaveBeenCalledWith('Task', '00T000000000001AAA');
    });
    expect(query).not.toHaveBeenCalledWith(expect.objectContaining({
      objectApiName: 'EmailMessage',
    }));
  });

  it('opens non-email task rows directly without probing EmailMessage', async () => {
    const query = vi.fn<CrmActivityQueryApi['query']>().mockImplementation(async request => {
      if (request.objectApiName === 'Task') {
        return [{
          Id: '00T000000000002AAA',
          Subject: 'Call: follow-up',
          ActivityDate: '2026-05-22',
          Type: 'Call',
          Status: 'Completed',
          IsClosed: true,
        }];
      }
      return [];
    });
    const getSchema = vi.fn().mockResolvedValue({ schema: { name: 'EmailMessage' } });
    const { navigateToRecord } = renderActivityTimelineAdapter({
      objectApiName: 'Lead',
      activityQueryApi: { query, getSchema } as CrmActivityQueryApi & {
        getSchema: typeof getSchema;
      },
    });

    await userEvent.click(await screen.findByText('Call: follow-up'));

    await waitFor(() => {
      expect(navigateToRecord).toHaveBeenCalledWith('Task', '00T000000000002AAA');
    });
    expect(getSchema).not.toHaveBeenCalled();
    expect(query).not.toHaveBeenCalledWith(expect.objectContaining({
      objectApiName: 'EmailMessage',
    }));
  });
});
