import { act, renderHook, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
  CRM_ACTIVITY_EMAIL_MESSAGE_ACTIVITY_FIELD,
  CRM_ACTIVITY_EMAIL_MESSAGE_OBJECT_NAME,
  CRM_ACTIVITY_EMAIL_TYPE,
  CRM_ACTIVITY_EVENT_FIELDS,
  CRM_ACTIVITY_EVENT_OBJECT_NAME,
  CRM_ACTIVITY_RELATION_FIELD_HINT,
  CRM_ACTIVITY_TASK_FIELDS,
  CRM_ACTIVITY_TASK_OBJECT_NAME,
  CRM_ACTIVITY_WHAT_LOOKUP_FIELD,
  CRM_ACTIVITY_WHO_LOOKUP_FIELD,
} from '../src/activity-timeline-policy';
import {
  useCrmActivities,
  type CrmActivityQueryApi,
} from '../src/activity-timeline-data';
import {
  buildCRMActivityTimelineRecordContext,
  crmActivityTimelineSurface,
  hasCRMActivityTimelineContext,
  isCrmEmailTaskActivity,
  resolveCRMActivityRelationField,
  resolveCRMActivityRelationFieldFromRecord,
} from '../src/activity-timeline-surface';

const queryMock = vi.fn<CrmActivityQueryApi['query']>();
const queryAllMock = vi.fn<NonNullable<CrmActivityQueryApi['queryAll']>>();
const activityQueryApi: CrmActivityQueryApi = {
  query: queryMock,
  queryAll: queryAllMock,
};

describe('crmActivityTimelineSurface', () => {
  it('owns the CRM activity query and relation facts in app-packs source', () => {
    expect(crmActivityTimelineSurface.relationFieldHintKey).toBe(CRM_ACTIVITY_RELATION_FIELD_HINT);
    expect(crmActivityTimelineSurface.relationFields).toEqual([
      CRM_ACTIVITY_WHO_LOOKUP_FIELD,
      CRM_ACTIVITY_WHAT_LOOKUP_FIELD,
    ]);
    expect(crmActivityTimelineSurface.activityObjects).toEqual([
      expect.objectContaining({
        objectApiName: CRM_ACTIVITY_TASK_OBJECT_NAME,
        selectFields: CRM_ACTIVITY_TASK_FIELDS,
        sortField: 'ActivityDate',
        isEvent: false,
        detailObjectApiName: CRM_ACTIVITY_TASK_OBJECT_NAME,
      }),
      expect.objectContaining({
        objectApiName: CRM_ACTIVITY_EVENT_OBJECT_NAME,
        selectFields: CRM_ACTIVITY_EVENT_FIELDS,
        sortField: 'StartDateTime',
        isEvent: true,
      }),
    ]);
    expect(crmActivityTimelineSurface.emailDetail).toEqual({
      taskType: CRM_ACTIVITY_EMAIL_TYPE,
      objectApiName: CRM_ACTIVITY_EMAIL_MESSAGE_OBJECT_NAME,
      activityLookupField: CRM_ACTIVITY_EMAIL_MESSAGE_ACTIVITY_FIELD,
      selectFields: ['Id', CRM_ACTIVITY_EMAIL_MESSAGE_ACTIVITY_FIELD, 'Subject'],
    });
  });

  it('resolves CRM WhoId versus WhatId policy without Nexus local constants', () => {
    expect(resolveCRMActivityRelationField(crmActivityTimelineSurface, 'Lead')).toBe('WhoId');
    expect(resolveCRMActivityRelationField(crmActivityTimelineSurface, 'Contact')).toBe('WhoId');
    expect(resolveCRMActivityRelationField(crmActivityTimelineSurface, 'Opportunity')).toBe('WhatId');
  });

  it('builds the relation hint consumed by quick-create renderers', () => {
    const context = buildCRMActivityTimelineRecordContext(
      crmActivityTimelineSurface,
      '003000000000001',
      'Lead',
      'Ada Lovelace',
    );

    expect(context).toEqual({
      relationField: 'WhoId',
      record: {
        Id: '003000000000001',
        Name: 'Ada Lovelace',
        WhoId: '003000000000001',
        _ActivityTimelineRelationField: 'WhoId',
      },
    });
    expect(hasCRMActivityTimelineContext(crmActivityTimelineSurface, context.record)).toBe(true);
    expect(resolveCRMActivityRelationFieldFromRecord(
      crmActivityTimelineSurface,
      context.record,
      'Account',
    )).toBe('WhoId');
  });

  it('recognizes an email activity by TaskSubtype even when Type is empty', () => {
    // Migrated (and SFDC-native) email tasks carry the discriminator in
    // TaskSubtype='Email' while Type is empty — this is the row that used to
    // fall back to the Task page instead of opening the EmailMessage.
    expect(isCrmEmailTaskActivity({
      isEvent: false,
      objectApiName: CRM_ACTIVITY_TASK_OBJECT_NAME,
      type: '',
      subtype: CRM_ACTIVITY_EMAIL_TYPE,
    })).toBe(true);
    // Compatibility: any path that sets Type='Email' still resolves.
    expect(isCrmEmailTaskActivity({
      isEvent: false,
      objectApiName: CRM_ACTIVITY_TASK_OBJECT_NAME,
      type: CRM_ACTIVITY_EMAIL_TYPE,
    })).toBe(true);
    // A call task (Type='Call', TaskSubtype='Task') is not an email.
    expect(isCrmEmailTaskActivity({
      isEvent: false,
      objectApiName: CRM_ACTIVITY_TASK_OBJECT_NAME,
      type: 'Call',
      subtype: 'Task',
    })).toBe(false);
    // Events are never email tasks, even if a subtype leaks through.
    expect(isCrmEmailTaskActivity({
      isEvent: true,
      objectApiName: CRM_ACTIVITY_EVENT_OBJECT_NAME,
      type: '',
      subtype: CRM_ACTIVITY_EMAIL_TYPE,
    })).toBe(false);
  });
});

describe('useCrmActivities', () => {
  beforeEach(() => {
    queryMock.mockReset();
    queryAllMock.mockReset();
    queryAllMock.mockResolvedValue([]);
    queryMock.mockImplementation(async ({ objectApiName }) => {
      if (objectApiName === 'Task') {
        return [{
          Id: '00T000000000001',
          Subject: 'Call back',
          ActivityDate: '2026-06-15',
          Status: 'Open',
          IsClosed: false,
        }];
      }
      return [{
        Id: '00U000000000001',
        Subject: 'Intro meeting',
        ActivityDate: '2026-06-16',
        StartDateTime: '2026-06-16T09:00:00Z',
      }];
    });
  });

  it('queries the app-declared activity objects with the app-declared relation field', async () => {
    const { result } = renderHook(() => useCrmActivities(
      '003000000000001',
      'Lead',
      activityQueryApi,
      crmActivityTimelineSurface,
    ));

    await waitFor(() => expect(result.current.loading).toBe(false));

    expect(queryMock).toHaveBeenCalledTimes(2);
    expect(queryMock).toHaveBeenCalledWith({
      objectApiName: 'Task',
      selectFields: [...crmActivityTimelineSurface.activityObjects[0].selectFields],
      filterExpr: "WhoId = '003000000000001'",
      sortField: 'ActivityDate',
      sortDirection: 'DESC',
      limit: 100,
    });
    expect(queryMock).toHaveBeenCalledWith({
      objectApiName: 'Event',
      selectFields: [...crmActivityTimelineSurface.activityObjects[1].selectFields],
      filterExpr: "WhoId = '003000000000001'",
      sortField: 'StartDateTime',
      sortDirection: 'DESC',
      limit: 100,
    });
    expect(result.current.totalCount).toBe(2);
  });

  it('treats a completed task as closed when IsClosed arrives as the active-formula "1" shape', async () => {
    queryMock.mockReset();
    queryMock.mockImplementation(async ({ objectApiName }) => {
      if (objectApiName === 'Task') {
        return [
          {
            Id: '00T000000000010',
            Subject: 'Weekly update',
            Type: 'Weekly Update',
            ActivityDate: '2026-06-01',
            Status: 'Completed',
            // Boolean active-formula fields surface as "1", not true.
            IsClosed: '1',
          },
          {
            Id: '00T000000000011',
            Subject: 'Follow up',
            ActivityDate: '2026-06-02',
            Status: 'Not Started',
            IsClosed: '0',
          },
        ];
      }
      return [];
    });

    const { result } = renderHook(() => useCrmActivities(
      '003000000000001',
      'Lead',
      activityQueryApi,
      crmActivityTimelineSurface,
    ));

    await waitFor(() => expect(result.current.loading).toBe(false));

    const completed = result.current.activities.find(item => item.id === '00T000000000010');
    const open = result.current.upcomingActivities.find(item => item.id === '00T000000000011');

    // Completed task is closed → past bucket, never rendered as overdue.
    expect(completed?.isClosed).toBe(true);
    expect(result.current.upcomingActivities.some(item => item.id === '00T000000000010')).toBe(false);
    // Open task still surfaces in the upcoming/overdue bucket.
    expect(open?.isClosed).toBe(false);
  });

  it('falls back to Status when IsClosed is null/stale so completed tasks are still closed', async () => {
    queryMock.mockReset();
    queryMock.mockImplementation(async ({ objectApiName }) => {
      if (objectApiName === 'Task') {
        return [{
          Id: '00T000000000020',
          Subject: 'Migrated task',
          ActivityDate: '2026-06-01',
          Status: 'Completed',
          IsClosed: null,
        }];
      }
      return [];
    });

    const { result } = renderHook(() => useCrmActivities(
      '003000000000001',
      'Lead',
      activityQueryApi,
      crmActivityTimelineSurface,
    ));

    await waitFor(() => expect(result.current.loading).toBe(false));

    const migrated = result.current.activities.find(item => item.id === '00T000000000020');
    expect(migrated?.isClosed).toBe(true);
    expect(result.current.upcomingActivities.some(item => item.id === '00T000000000020')).toBe(false);
  });

  it('exposes Task.TaskSubtype so migrated email activities are recognizable when Type is empty', async () => {
    queryMock.mockReset();
    queryMock.mockImplementation(async ({ objectApiName }) => {
      if (objectApiName === 'Task') {
        return [{
          Id: '00T000000000030',
          Subject: 'メール: Re: proposal',
          ActivityDate: '2026-05-20',
          Status: 'Completed',
          IsClosed: '1',
          // Migrated email task: Type is empty; the discriminator is TaskSubtype.
          TaskSubtype: 'Email',
        }];
      }
      return [];
    });

    const { result } = renderHook(() => useCrmActivities(
      '003000000000001',
      'Lead',
      activityQueryApi,
      crmActivityTimelineSurface,
    ));

    await waitFor(() => expect(result.current.loading).toBe(false));

    // The Task query must select TaskSubtype for the discriminator to arrive.
    expect(CRM_ACTIVITY_TASK_FIELDS).toContain('TaskSubtype');
    const emailItem = result.current.activities.find(item => item.id === '00T000000000030');
    expect(emailItem?.type).toBe('');
    expect(emailItem?.subtype).toBe('Email');
    expect(isCrmEmailTaskActivity({
      isEvent: emailItem?.isEvent ?? true,
      objectApiName: emailItem?.objectApiName ?? '',
      type: emailItem?.type ?? '',
      subtype: emailItem?.subtype,
    })).toBe(true);
  });

  it('resolves all email subjects and detail targets with two bounded EmailMessage queries', async () => {
    queryMock.mockReset();
    queryAllMock.mockResolvedValue([
      { ActivityId: '00T000000000031', emailMessageId: '02s000000000001' },
      { ActivityId: '00T000000000032', emailMessageId: '02s000000000002' },
    ]);
    queryMock.mockImplementation(async ({ objectApiName }) => {
      if (objectApiName === 'Task') {
        return [
          {
            Id: '00T000000000031', Subject: 'Email', TaskSubtype: 'Email',
            ActivityDate: '2026-08-28', Status: 'Completed', IsClosed: '1',
          },
          {
            Id: '00T000000000032', Subject: 'Email', TaskSubtype: 'Email',
            ActivityDate: '2026-08-29', Status: 'Completed', IsClosed: '1',
          },
        ];
      }
      if (objectApiName === 'EmailMessage') {
        return [
          { Id: '02s000000000001', ActivityId: '00T000000000031', Subject: 'First composed subject' },
          { Id: '02s000000000002', ActivityId: '00T000000000032', Subject: '二番目の件名' },
        ];
      }
      return [];
    });

    const { result } = renderHook(() => useCrmActivities(
      '003000000000001', 'Contact', activityQueryApi, crmActivityTimelineSurface,
    ));

    await waitFor(() => expect(result.current.loading).toBe(false));

    expect(queryMock).toHaveBeenCalledTimes(3);
    expect(queryAllMock).toHaveBeenCalledWith(
      "SELECT ActivityId, MIN(Id) emailMessageId FROM EmailMessage WHERE ActivityId IN ('00T000000000031', '00T000000000032') AND IsDeleted = false GROUP BY ActivityId LIMIT 2",
    );
    expect(queryMock).toHaveBeenCalledWith(expect.objectContaining({
      objectApiName: 'EmailMessage',
      selectFields: ['Id', 'ActivityId', 'Subject'],
      filterExpr: "Id IN ('02s000000000001', '02s000000000002')",
      limit: 2,
    }));
    expect(result.current.activities).toEqual(expect.arrayContaining([
      expect.objectContaining({
        id: '00T000000000031', subject: 'First composed subject',
        detailObjectApiName: 'EmailMessage', detailRecordId: '02s000000000001',
      }),
      expect.objectContaining({
        id: '00T000000000032', subject: '二番目の件名',
        detailObjectApiName: 'EmailMessage', detailRecordId: '02s000000000002',
      }),
    ]));
  });

  it('cannot let duplicate EmailMessages for one Task starve later Tasks', async () => {
    queryMock.mockReset();
    queryAllMock.mockResolvedValue([
      { ActivityId: '00T000000000041', emailMessageId: '02s000000000011' },
      { ActivityId: '00T000000000042', emailMessageId: '02s000000000099' },
    ]);
    queryMock.mockImplementation(async ({ objectApiName }) => {
      if (objectApiName === 'Task') {
        return [
          {
            Id: '00T000000000041', Subject: 'Email', TaskSubtype: 'Email',
            Status: 'Completed', IsClosed: '1',
          },
          {
            Id: '00T000000000042', Subject: 'Email', TaskSubtype: 'Email',
            Status: 'Completed', IsClosed: '1',
          },
        ];
      }
      if (objectApiName === 'EmailMessage') {
        return [
          { Id: '02s000000000011', ActivityId: '00T000000000041', Subject: 'First Task' },
          { Id: '02s000000000099', ActivityId: '00T000000000042', Subject: 'Later Task' },
        ];
      }
      return [];
    });

    const { result } = renderHook(() => useCrmActivities(
      '003000000000001', 'Lead', activityQueryApi, crmActivityTimelineSurface,
    ));
    await waitFor(() => expect(result.current.loading).toBe(false));

    expect(result.current.activities).toEqual(expect.arrayContaining([
      expect.objectContaining({ id: '00T000000000041', subject: 'First Task' }),
      expect.objectContaining({ id: '00T000000000042', subject: 'Later Task' }),
    ]));
  });

  it('cannot let an older record request overwrite a newer timeline', async () => {
    queryMock.mockReset();
    let resolveOlderTask: ((records: Record<string, unknown>[]) => void) | undefined;
    const olderTask = new Promise<Record<string, unknown>[]>(resolve => {
      resolveOlderTask = resolve;
    });
    queryMock.mockImplementation(async request => {
      if (request.objectApiName === 'Event') {
        return [];
      }
      if (request.filterExpr.includes('003000000000041')) {
        return olderTask;
      }
      return [{
        Id: '00T000000000052', Subject: 'Newer record activity',
        Status: 'Completed', IsClosed: '1',
      }];
    });

    const { result, rerender } = renderHook(
      ({ recordId }) => useCrmActivities(
        recordId, 'Lead', activityQueryApi, crmActivityTimelineSurface,
      ),
      { initialProps: { recordId: '003000000000041' } },
    );
    await waitFor(() => expect(queryMock).toHaveBeenCalledWith(expect.objectContaining({
      filterExpr: "WhoId = '003000000000041'",
    })));

    rerender({ recordId: '003000000000052' });
    await waitFor(() => expect(result.current.activities).toEqual([
      expect.objectContaining({ id: '00T000000000052', subject: 'Newer record activity' }),
    ]));

    await act(async () => {
      resolveOlderTask?.([{
        Id: '00T000000000041', Subject: 'Stale record activity',
        Status: 'Completed', IsClosed: '1',
      }]);
      await olderTask;
    });
    expect(result.current.activities).toEqual([
      expect.objectContaining({ id: '00T000000000052', subject: 'Newer record activity' }),
    ]);
  });

  it('clears the previous record timeline synchronously on navigation', async () => {
    queryMock.mockReset();
    let resolveNewerTask: ((records: Record<string, unknown>[]) => void) | undefined;
    const newerTask = new Promise<Record<string, unknown>[]>(resolve => {
      resolveNewerTask = resolve;
    });
    queryMock.mockImplementation(async request => {
      if (request.objectApiName === 'Event') {
        return [];
      }
      if (request.filterExpr.includes('003000000000062')) {
        return newerTask;
      }
      return [{
        Id: '00T000000000061', Subject: 'Previous record activity',
        Status: 'Completed', IsClosed: '1',
      }];
    });

    const { result, rerender } = renderHook(
      ({ recordId }) => useCrmActivities(
        recordId, 'Lead', activityQueryApi, crmActivityTimelineSurface,
      ),
      { initialProps: { recordId: '003000000000061' } },
    );
    await waitFor(() => expect(result.current.activities).toEqual([
      expect.objectContaining({ id: '00T000000000061' }),
    ]));

    rerender({ recordId: '003000000000062' });
    expect(result.current.activities).toEqual([]);
    expect(result.current.loading).toBe(true);

    await act(async () => {
      resolveNewerTask?.([{
        Id: '00T000000000062', Subject: 'Current record activity',
        Status: 'Completed', IsClosed: '1',
      }]);
      await newerTask;
    });
    await waitFor(() => expect(result.current.activities).toEqual([
      expect.objectContaining({ id: '00T000000000062', subject: 'Current record activity' }),
    ]));
  });

  it('refetches when only the host object changes across the same relation field', async () => {
    queryMock.mockReset();
    let hostObject = 'Lead';
    queryMock.mockImplementation(async request => {
      if (request.objectApiName === 'Event') {
        return [];
      }
      return [{
        Id: hostObject === 'Lead' ? '00T000000000071' : '00T000000000072',
        Subject: `${hostObject} activity`,
        Status: 'Completed',
        IsClosed: '1',
      }];
    });

    const { result, rerender } = renderHook(
      ({ objectApiName }) => useCrmActivities(
        '003000000000071', objectApiName, activityQueryApi, crmActivityTimelineSurface,
      ),
      { initialProps: { objectApiName: 'Lead' } },
    );
    await waitFor(() => expect(result.current.activities).toEqual([
      expect.objectContaining({ id: '00T000000000071', subject: 'Lead activity' }),
    ]));

    hostObject = 'Contact';
    rerender({ objectApiName: 'Contact' });
    expect(result.current.activities).toEqual([]);
    expect(result.current.loading).toBe(true);

    await waitFor(() => expect(result.current.activities).toEqual([
      expect.objectContaining({ id: '00T000000000072', subject: 'Contact activity' }),
    ]));
    expect(queryMock).toHaveBeenCalledTimes(4);
  });
});
