/**
 * @file workflow-email-notifications.test.ts
 * @description Tests for notifyStakeholdersOnTransition + resolveNextActors.
 * Verifies that the correct personas receive the correct channels at each
 * onboarding, resignation, and termination stage.
 *
 * SRS Coverage:
 *  §6.14.3 — Next actor gets email + WhatsApp + in-app
 *  §6.14.3 — Send Back notifies ASM via email + WhatsApp + in-app
 *  §6.12.3 — Rejection notifies applicant via email + WhatsApp
 *  §6.13   — Work Notes / observer roles get in-app only on terminal events
 */

import {
  notifyStakeholdersOnTransition,
  resolveNextActors,
  notifyResignationSubmittedEmails,
  notifyRelocationSubmittedEmails,
  notifyConstitutionalSubmittedEmails,
} from '../common/utils/workflow-email-notifications.js';
import { NotificationService } from '../services/NotificationService.js';
import { sendEmail } from '../common/utils/email.service.js';

// ─── Mocks ───────────────────────────────────────────────────────────────────

jest.mock('../common/utils/email.service.js', () => ({
  sendEmail: jest.fn().mockResolvedValue(true),
}));

const mockNotify = jest.spyOn(NotificationService, 'notify').mockResolvedValue(undefined);

// Helper: build a participant user object
const makeUser = (overrides: Partial<{
  id: string; email: string; fullName: string; roleCode: string; mobileNumber: string | null;
}> = {}) => ({
  id: overrides.id ?? 'user-1',
  email: overrides.email ?? 'user@re.com',
  fullName: overrides.fullName ?? 'Test User',
  roleCode: overrides.roleCode ?? 'DD_ADMIN',
  mobileNumber: overrides.mobileNumber ?? '+919800000001',
  ...overrides,
});

const makeParticipant = (user: ReturnType<typeof makeUser>) => ({ user });

// ─── Mock DB ─────────────────────────────────────────────────────────────────

const mockParticipants: ReturnType<typeof makeParticipant>[] = [];

jest.mock('../database/models/index.js', () => ({
  default: {
    RequestParticipant: {
      findAll: jest.fn(async () => mockParticipants),
    },
    User: {
      findByPk: jest.fn(async (id: string) => ({
        id,
        fullName: 'System User',
        email: 'sysuser@re.com',
        mobileNumber: '+910000000000',
      })),
    },
    Outlet: { findByPk: jest.fn().mockResolvedValue(null) },
    District: {},
    StageApprovalAction: { findAll: jest.fn().mockResolvedValue([]) },
  },
}));

process.env.FRONTEND_URL = 'http://localhost:5173';

// ─── Shared metadata ──────────────────────────────────────────────────────────

const baseMeta = {
  code: 'RES-2026-0001',
  dealerName: 'Sunrise Motorcycles',
  dealerId: 'dealer-99',
  actionUserFullName: 'Ravi Kumar (ASM)',
  action: 'Forwarded to RBM',
  remarks: 'Documents verified.',
  link: 'http://localhost:5173/resignation/abc123',
};

// ─── resolveNextActors ────────────────────────────────────────────────────────

describe('resolveNextActors — stage-to-role mapping', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    mockParticipants.length = 0;
  });

  it('TC-RNA-01: Level 1 Interview resolves to DD_ZM and RBM', async () => {
    const zmUser = makeUser({ id: 'zm-1', roleCode: 'DD_ZM' });
    const rbmUser = makeUser({ id: 'rbm-1', roleCode: 'RBM' });
    mockParticipants.push(makeParticipant(zmUser), makeParticipant(rbmUser));

    const actors = await resolveNextActors('app-1', 'application', 'Level 1 Interview');
    expect(actors).toContain('zm-1');
    expect(actors).toContain('rbm-1');
  });

  it('TC-RNA-02: Level 2 Interview resolves to ZBH and DD_LEAD', async () => {
    const zbhUser = makeUser({ id: 'zbh-1', roleCode: 'ZBH' });
    const leadUser = makeUser({ id: 'lead-1', roleCode: 'DD_LEAD' });
    mockParticipants.push(makeParticipant(zbhUser), makeParticipant(leadUser));

    const actors = await resolveNextActors('app-1', 'application', 'Level 2 Interview');
    expect(actors).toContain('zbh-1');
    expect(actors).toContain('lead-1');
  });

  it('TC-RNA-03: Level 3 Interview resolves to NBH and DD_HEAD', async () => {
    const nbhUser = makeUser({ id: 'nbh-1', roleCode: 'NBH' });
    const headUser = makeUser({ id: 'head-1', roleCode: 'DD_HEAD' });
    mockParticipants.push(makeParticipant(nbhUser), makeParticipant(headUser));

    const actors = await resolveNextActors('app-1', 'application', 'Level 3 Interview');
    expect(actors).toContain('nbh-1');
    expect(actors).toContain('head-1');
  });

  it('TC-RNA-04: LOI Approval resolves to DD_HEAD (DD Head has not yet approved)', async () => {
    const headUser = makeUser({ id: 'head-1', roleCode: 'DD_HEAD' });
    const nbhUser = makeUser({ id: 'nbh-1', roleCode: 'NBH' });
    mockParticipants.push(makeParticipant(headUser), makeParticipant(nbhUser));

    // StageApprovalAction returns empty (no approvals yet)
    const db = (await import('../database/models/index.js')).default as any;
    db.StageApprovalAction.findAll.mockResolvedValueOnce([]);

    const actors = await resolveNextActors('app-1', 'application', 'LOI Approval');
    // Sequential: DD Head first
    expect(actors).toContain('head-1');
    expect(actors).not.toContain('nbh-1');
  });

  it('TC-RNA-05: LOI Approval resolves to NBH after DD Head has approved', async () => {
    const headUser = makeUser({ id: 'head-1', roleCode: 'DD_HEAD' });
    const nbhUser = makeUser({ id: 'nbh-1', roleCode: 'NBH' });
    mockParticipants.push(makeParticipant(headUser), makeParticipant(nbhUser));

    const db = (await import('../database/models/index.js')).default as any;
    db.StageApprovalAction.findAll.mockResolvedValueOnce([
      { actorRole: 'DD Head' }, // DD Head already approved
    ]);

    const actors = await resolveNextActors('app-1', 'application', 'LOI Approval');
    expect(actors).toContain('nbh-1');
    expect(actors).not.toContain('head-1');
  });

  it('TC-RNA-06: NBH Approval stage resolves to NBH only', async () => {
    const nbhUser = makeUser({ id: 'nbh-1', roleCode: 'NBH' });
    mockParticipants.push(makeParticipant(nbhUser));

    const actors = await resolveNextActors('res-1', 'resignation', 'NBH Approval');
    expect(actors).toEqual(['nbh-1']);
  });

  it('TC-RNA-07: Legal Review stage resolves to LEGAL_ADMIN', async () => {
    const legalUser = makeUser({ id: 'legal-1', roleCode: 'LEGAL_ADMIN' });
    mockParticipants.push(makeParticipant(legalUser));

    const actors = await resolveNextActors('res-1', 'resignation', 'Legal Review');
    expect(actors).toContain('legal-1');
  });

  it('TC-RNA-08: Unknown stage returns empty array (no crash)', async () => {
    const actors = await resolveNextActors('app-1', 'application', 'NonExistentStage');
    expect(actors).toEqual([]);
  });
});

// ─── notifyStakeholdersOnTransition ──────────────────────────────────────────

describe('notifyStakeholdersOnTransition — channel selection per persona', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    mockParticipants.length = 0;
  });

  it('TC-NST-01: Next actor receives system + email + whatsapp channels', async () => {
    const rbmUser = makeUser({ id: 'rbm-1', roleCode: 'RBM', mobileNumber: '+919800000001' });
    mockParticipants.push(makeParticipant(rbmUser));

    await notifyStakeholdersOnTransition('app-1', 'application', 'RBM Review', baseMeta);

    expect(mockNotify).toHaveBeenCalledWith(
      'rbm-1',
      'user@re.com',
      expect.objectContaining({
        channels: expect.arrayContaining(['system', 'email', 'whatsapp']),
        templateCode: 'WORKFLOW_ACTION_REQUIRED',
      })
    );
  });

  it('TC-NST-02: Next actor without phone gets system + email only (no WhatsApp)', async () => {
    const zbhUser = makeUser({ id: 'zbh-1', roleCode: 'ZBH', mobileNumber: null });
    mockParticipants.push(makeParticipant(zbhUser));

    await notifyStakeholdersOnTransition('app-1', 'application', 'ZBH Review', baseMeta);

    expect(mockNotify).toHaveBeenCalledWith(
      'zbh-1',
      'user@re.com',
      expect.objectContaining({
        channels: expect.not.arrayContaining(['whatsapp']),
      })
    );
  });

  it('TC-NST-03: Send Back action notifies ASM via system + email + whatsapp', async () => {
    const asmUser = makeUser({ id: 'asm-1', roleCode: 'ASM', mobileNumber: '+91900' });
    mockParticipants.push(makeParticipant(asmUser));

    await notifyStakeholdersOnTransition('res-1', 'resignation', 'ASM Review', {
      ...baseMeta,
      action: 'Sent Back to ASM for clarification',
      actionUserFullName: 'DD Lead User', // different from ASM — won't be skipped
    });

    expect(mockNotify).toHaveBeenCalledWith(
      'asm-1',
      expect.any(String),
      expect.objectContaining({
        channels: expect.arrayContaining(['system', 'email', 'whatsapp']),
        templateCode: 'WORKFLOW_ACTION_REQUIRED',
      })
    );
  });

  it('TC-NST-04: Dealer receives system + email + whatsapp on Rejected terminal event', async () => {
    const dealerUser = makeUser({
      id: 'dealer-99',
      roleCode: 'Dealer',
      mobileNumber: '+91987654321',
    });
    mockParticipants.push(makeParticipant(dealerUser));

    await notifyStakeholdersOnTransition('res-1', 'resignation', 'Rejected', {
      ...baseMeta,
      dealerId: 'dealer-99',
    });

    expect(mockNotify).toHaveBeenCalledWith(
      'dealer-99',
      expect.any(String),
      expect.objectContaining({
        channels: expect.arrayContaining(['system', 'email', 'whatsapp']),
      })
    );
  });

  it('TC-NST-05: Dealer receives in-app only on non-terminal interim stage', async () => {
    const dealerUser = makeUser({
      id: 'dealer-99',
      roleCode: 'Dealer',
      mobileNumber: '+91987654321',
    });
    mockParticipants.push(makeParticipant(dealerUser));

    await notifyStakeholdersOnTransition('res-1', 'resignation', 'ZBH Review', {
      ...baseMeta,
      dealerId: 'dealer-99',
    });

    const dealerCall = mockNotify.mock.calls.find((c) => c[0] === 'dealer-99');
    expect(dealerCall?.[2].channels).toEqual(['system']);
  });

  it('TC-NST-06: Key observers (DD_LEAD, DD_HEAD, NBH) receive in-app only on terminal events', async () => {
    const leadUser = makeUser({ id: 'lead-1', roleCode: 'DD_LEAD', mobileNumber: '+91900' });
    mockParticipants.push(makeParticipant(leadUser));

    await notifyStakeholdersOnTransition('res-1', 'resignation', 'Completed', {
      ...baseMeta,
      dealerId: 'dealer-99',
      actionUserFullName: 'DD Lead User', // acting user – but leadUser will still match key observer
    });

    const observerCall = mockNotify.mock.calls.find((c) => c[0] === 'lead-1');
    // Key observer: only in-app (system)
    expect(observerCall?.[2].channels).toEqual(['system']);
  });

  it('TC-NST-07: Acting user is skipped to avoid self-notification', async () => {
    const actingUser = makeUser({
      id: 'acting-1',
      roleCode: 'ZBH',
      fullName: 'Ravi Kumar (ASM)', // same as baseMeta.actionUserFullName
    });
    // actingUser is NOT the next actor (no role match for ZBH Review mapping)
    mockParticipants.push(makeParticipant(actingUser));

    await notifyStakeholdersOnTransition('app-1', 'application', 'ZBH Review', baseMeta);

    // Should not notify the acting user (they are the one who just acted)
    const actingCall = mockNotify.mock.calls.find((c) => c[0] === 'acting-1');
    expect(actingCall).toBeUndefined();
  });
});

// ─── notifyResignationSubmittedEmails ─────────────────────────────────────────

describe('notifyResignationSubmittedEmails — channels on dealer submission', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    mockParticipants.length = 0;
  });

  it('TC-NRSE-01: sends RESIGNATION_RECEIVED email to dealer on submission', async () => {
    const db = (await import('../database/models/index.js')).default as any;
    db.User.findByPk.mockResolvedValueOnce({
      id: 'dealer-1',
      email: 'dealer@example.com',
      fullName: 'Sunrise Dealers',
      mobileNumber: '+919876543210',
    });

    const sendEmailMock = sendEmail as jest.Mock;

    await notifyResignationSubmittedEmails({
      id: 'res-uuid-1',
      dealerId: 'dealer-1',
      resignationId: 'RES-2026-0001',
      lastOperationalDateSales: '2026-06-30',
    });

    expect(sendEmailMock).toHaveBeenCalledWith(
      'dealer@example.com',
      expect.stringContaining('RES-2026-0001'),
      'RESIGNATION_RECEIVED',
      expect.objectContaining({ dealerName: 'Sunrise Dealers' })
    );
  });

  it('TC-NRSE-02: sends WhatsApp to dealer when mobileNumber is present', async () => {
    const db = (await import('../database/models/index.js')).default as any;
    db.User.findByPk.mockResolvedValueOnce({
      id: 'dealer-1',
      email: 'dealer@example.com',
      fullName: 'Sunrise Dealers',
      mobileNumber: '+919876543210',
    });

    await notifyResignationSubmittedEmails({
      id: 'res-uuid-1',
      dealerId: 'dealer-1',
      resignationId: 'RES-2026-0001',
      lastOperationalDateSales: '2026-06-30',
    });

    const whatsappCall = mockNotify.mock.calls.find(
      (c) => c[2].channels?.includes('whatsapp')
    );
    expect(whatsappCall).toBeDefined();
    expect(whatsappCall?.[2].templateCode).toBe('RESIGNATION_RECEIVED');
  });

  it('TC-NRSE-03: notifies internal participants via email + system + whatsapp', async () => {
    const db = (await import('../database/models/index.js')).default as any;
    db.User.findByPk.mockResolvedValueOnce({
      id: 'dealer-1',
      email: 'dealer@example.com',
      fullName: 'Sunrise Dealers',
      mobileNumber: '+919876543210',
    });

    const internalUser = makeUser({ id: 'asm-1', roleCode: 'ASM', mobileNumber: '+91111' });
    mockParticipants.push(makeParticipant(internalUser));

    await notifyResignationSubmittedEmails({
      id: 'res-uuid-1',
      dealerId: 'dealer-1',
      resignationId: 'RES-2026-0001',
    });

    const internalCall = mockNotify.mock.calls.find((c) => c[0] === 'asm-1');
    expect(internalCall?.[2].channels).toEqual(
      expect.arrayContaining(['email', 'system', 'whatsapp'])
    );
    expect(internalCall?.[2].templateCode).toBe('RESIGNATION_SUBMITTED');
  });

  it('TC-NRSE-04: skips WhatsApp if dealer has no mobileNumber', async () => {
    const db = (await import('../database/models/index.js')).default as any;
    db.User.findByPk.mockResolvedValueOnce({
      id: 'dealer-2',
      email: 'dealer2@example.com',
      fullName: 'No Phone Dealer',
      mobileNumber: null,
    });

    await notifyResignationSubmittedEmails({
      id: 'res-uuid-2',
      dealerId: 'dealer-2',
      resignationId: 'RES-2026-0002',
    });

    const whatsappCall = mockNotify.mock.calls.find(
      (c) => c[0] === 'dealer-2' && c[2].channels?.includes('whatsapp')
    );
    expect(whatsappCall).toBeUndefined();
  });
});
