/**
 * @file workflow-service.test.ts
 * @description Tests for WorkflowService.transitionApplication — verifies that
 * each status transition triggers the correct notifications, audit log entries,
 * and SLA tracking calls.
 *
 * SRS Coverage:
 *  §6.22   — Every workflow event is auto-logged in Audit Trail
 *  §6.14.3 — Status transitions trigger in-system + email + WhatsApp for applicant
 *  §6.16   — LOI Issued triggers LOI_ISSUED email template (not WhatsApp per SRS §1.1.2)
 *  §9.4    — SLA tracking starts/stops on each stage transition
 */

import { WorkflowService } from '../services/WorkflowService.js';
import { NotificationService } from '../services/NotificationService.js';
import { SLAService } from '../services/SLAService.js';

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

jest.mock('../database/models/index.js', () => {
  const mockUpdate = jest.fn().mockResolvedValue(true);
  const mockCreate = jest.fn().mockResolvedValue({ id: 'hist-1' });
  const mockFindOne = jest.fn().mockResolvedValue(null);
  const mockFindByPk = jest.fn().mockResolvedValue({ fullName: 'Test Actor' });

  return {
    default: {
      Application: {},
      ApplicationStatusHistory: { create: mockCreate },
      User: { findOne: mockFindOne, findByPk: mockFindByPk },
      Dealer: {},
      StageApprovalPolicy: { findOne: jest.fn().mockResolvedValue(null) },
      StageApprovalAction: { findAll: jest.fn().mockResolvedValue([]) },
    },
    __mocks__: { mockUpdate, mockCreate, mockFindOne },
  };
});

jest.mock('../common/utils/progress.js', () => ({
  syncApplicationProgress: jest.fn().mockResolvedValue(undefined),
  PIPELINE_STAGE_LABEL_BY_OVERALL_STATUS: {},
}));

jest.mock('../common/config/constants.js', () => ({
  AUDIT_ACTIONS: { UPDATED: 'UPDATED' },
  APPLICATION_STAGES: { SHORTLISTED: 'Shortlisted', LOI_ISSUED: 'LOI Issued' },
  OVERALL_STATUS_TO_DB_CURRENT_STAGE: {},
}));

jest.mock('../common/utils/workflow-email-notifications.js', () => ({
  notifyStakeholdersOnTransition: jest.fn().mockResolvedValue(undefined),
}));

jest.mock('../services/applicationAuditLog.service.js', () => ({
  pickApplicationAuditContext: jest.fn().mockReturnValue({}),
  safeAuditLogCreate: jest.fn().mockResolvedValue(undefined),
}));

const mockSLAStop = jest.spyOn(SLAService, 'stopTrack').mockResolvedValue(undefined as any);
const mockSLAStart = jest.spyOn(SLAService, 'startTrack').mockResolvedValue(undefined as any);
const mockNotify = jest.spyOn(NotificationService, 'notify').mockResolvedValue(undefined);

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

// ─── Application fixture ─────────────────────────────────────────────────────

const makeApp = (overrides: Record<string, any> = {}) => {
  const app: any = {
    id: 'app-uuid-001',
    applicationId: 'APP-2026-001',
    overallStatus: 'Shortlisted',
    currentStage: 'Shortlisted',
    progressPercentage: 20,
    email: 'applicant@gmail.com',
    applicantName: 'Rahul Verma',
    mobileNumber: '+919876543210',
    update: jest.fn().mockResolvedValue(true),
    ...overrides,
  };
  return app;
};

// ─── Tests ────────────────────────────────────────────────────────────────────

describe('WorkflowService.transitionApplication — status transitions', () => {
  beforeEach(() => jest.clearAllMocks());

  it('TC-WS-01: updates application overallStatus to the target status', async () => {
    const app = makeApp();
    await WorkflowService.transitionApplication(app, 'Level 1 Interview Pending', 'user-1');
    expect(app.update).toHaveBeenCalledWith(
      expect.objectContaining({ overallStatus: 'Level 1 Interview Pending' })
    );
  });

  it('TC-WS-02: creates an ApplicationStatusHistory record on each transition', async () => {
    const db = (await import('../database/models/index.js')).default as any;
    const app = makeApp();
    await WorkflowService.transitionApplication(app, 'Level 2 Interview Pending', 'user-2');
    expect(db.ApplicationStatusHistory.create).toHaveBeenCalledWith(
      expect.objectContaining({
        applicationId: 'app-uuid-001',
        previousStatus: 'Shortlisted',
        newStatus: 'Level 2 Interview Pending',
        changedBy: 'user-2',
      })
    );
  });

  it('TC-WS-03: skips redundant status history when status is already at target', async () => {
    const db = (await import('../database/models/index.js')).default as any;
    const app = makeApp({ overallStatus: 'Level 1 Interview Pending' });
    await WorkflowService.transitionApplication(app, 'Level 1 Interview Pending', 'user-1');
    expect(db.ApplicationStatusHistory.create).not.toHaveBeenCalled();
  });

  it('TC-WS-04: calls safeAuditLogCreate with oldData and newData on each transition', async () => {
    const { safeAuditLogCreate } = await import('../services/applicationAuditLog.service.js');
    const app = makeApp();
    await WorkflowService.transitionApplication(app, 'FDD Verification', 'user-fdd');
    expect(safeAuditLogCreate).toHaveBeenCalledWith(
      expect.objectContaining({
        action: 'UPDATED',
        entityType: 'application',
        entityId: 'app-uuid-001',
        oldData: expect.objectContaining({ status: 'Shortlisted' }),
        newData: expect.objectContaining({ status: 'FDD Verification' }),
      })
    );
  });

  it('TC-WS-05: stops SLA tracking for the previous stage and starts for the new stage', async () => {
    const app = makeApp({ currentStage: 'Level 1 Interview Pending' });
    await WorkflowService.transitionApplication(app, 'FDD Verification', 'user-1');
    expect(mockSLAStop).toHaveBeenCalledWith('app-uuid-001', 'Level 1 Interview Pending');
  });

  it('TC-WS-06: triggers NotificationService.notify for the applicant on any status transition', async () => {
    const db = (await import('../database/models/index.js')).default as any;
    db.User.findOne.mockResolvedValueOnce({ id: 'sys-user-1', mobileNumber: '+91123' });
    const app = makeApp();
    await WorkflowService.transitionApplication(app, 'Level 1 Interview Pending', 'user-admin');
    expect(mockNotify).toHaveBeenCalledWith(
      expect.any(String),
      'applicant@gmail.com',
      expect.objectContaining({
        title: expect.stringContaining('Onboarding Update'),
        channels: expect.arrayContaining(['email', 'system']),
      })
    );
  });

  it('TC-WS-07: uses LOI_ISSUED template when target status is "LOI Issued"', async () => {
    const db = (await import('../database/models/index.js')).default as any;
    db.User.findOne.mockResolvedValueOnce({ id: 'sys-user-1', mobileNumber: '+91123' });
    const app = makeApp();
    await WorkflowService.transitionApplication(app, 'LOI Issued', 'admin-1');
    expect(mockNotify).toHaveBeenCalledWith(
      expect.anything(),
      'applicant@gmail.com',
      expect.objectContaining({ templateCode: 'LOI_ISSUED' })
    );
  });

  it('TC-WS-08: uses LOA_ISSUED template when target status is "LOA Issued"', async () => {
    const db = (await import('../database/models/index.js')).default as any;
    db.User.findOne.mockResolvedValueOnce({ id: 'sys-user-1', mobileNumber: '+91123' });
    const app = makeApp();
    await WorkflowService.transitionApplication(app, 'LOA Issued', 'admin-1');
    expect(mockNotify).toHaveBeenCalledWith(
      expect.anything(),
      'applicant@gmail.com',
      expect.objectContaining({ templateCode: 'LOA_ISSUED' })
    );
  });

  it('TC-WS-09: uses DEALER_CODE_READY template when target status is "Dealer Code Generated"', async () => {
    const db = (await import('../database/models/index.js')).default as any;
    db.User.findOne.mockResolvedValueOnce({ id: 'sys-user-1', mobileNumber: null });
    const app = makeApp();
    await WorkflowService.transitionApplication(app, 'Dealer Code Generated', 'admin-1');
    expect(mockNotify).toHaveBeenCalledWith(
      expect.anything(),
      'applicant@gmail.com',
      expect.objectContaining({ templateCode: 'DEALER_CODE_READY' })
    );
  });

  it('TC-WS-10: notifyStakeholdersOnTransition is called for every transition', async () => {
    const { notifyStakeholdersOnTransition } = await import('../common/utils/workflow-email-notifications.js');
    const app = makeApp();
    await WorkflowService.transitionApplication(app, 'ZBH Review', 'user-zbh');
    expect(notifyStakeholdersOnTransition).toHaveBeenCalledWith(
      'app-uuid-001',
      'application',
      'ZBH Review',
      expect.objectContaining({
        code: 'APP-2026-001',
        dealerName: 'Rahul Verma',
      })
    );
  });

  it('TC-WS-11: skips applicant notification when skipNotification metadata is true', async () => {
    const app = makeApp();
    await WorkflowService.transitionApplication(app, 'Shortlisted', 'admin-1', {
      skipNotification: true,
      forceLog: true,
    });
    expect(mockNotify).not.toHaveBeenCalled();
  });

  it('TC-WS-12: includes applicant mobileNumber in WhatsApp placeholders', async () => {
    const db = (await import('../database/models/index.js')).default as any;
    db.User.findOne.mockResolvedValueOnce({
      id: 'sys-user-1',
      mobileNumber: '+919876543210',
    });
    const app = makeApp();
    await WorkflowService.transitionApplication(app, 'FDD Verification', 'admin-1');
    const notifyCall = mockNotify.mock.calls.find(
      (c) => c[1] === 'applicant@gmail.com'
    );
    expect(notifyCall?.[2].placeholders?.phone).toBeTruthy();
  });
});

// ─── WorkflowService.evaluateStagePolicy ─────────────────────────────────────

describe('WorkflowService.evaluateStagePolicy — multi-role gate logic', () => {
  beforeEach(() => jest.clearAllMocks());

  it('TC-WESP-01: returns policyMet=true when no active policy exists for the stage', async () => {
    const db = (await import('../database/models/index.js')).default as any;
    db.StageApprovalPolicy.findOne.mockResolvedValueOnce(null);
    const result = await WorkflowService.evaluateStagePolicy('app-1', 'SOME_STAGE');
    expect(result.policyMet).toBe(true);
  });

  it('TC-WESP-02: Super Admin bypass — always returns policyMet=true', async () => {
    const db = (await import('../database/models/index.js')).default as any;
    db.StageApprovalPolicy.findOne.mockResolvedValueOnce({
      requiredRoles: ['DD_ZM', 'RBM'],
      approvalMode: 'ALL',
      minApprovals: 2,
    });
    db.StageApprovalAction.findAll.mockResolvedValueOnce([
      { actorUserId: 'su-1', actorRole: 'Super Admin', decision: 'Approved' },
    ]);
    const result = await WorkflowService.evaluateStagePolicy('app-1', 'LEVEL_1');
    expect(result.policyMet).toBe(true);
    expect(result.overriddenBy).toBe('Super Admin');
  });

  it('TC-WESP-03: MIN_N mode — policyMet=true when minApprovals threshold is reached', async () => {
    const db = (await import('../database/models/index.js')).default as any;
    db.StageApprovalPolicy.findOne.mockResolvedValueOnce({
      requiredRoles: ['DD_ZM', 'RBM'],
      approvalMode: 'MIN_N',
      minApprovals: 1,
    });
    db.StageApprovalAction.findAll.mockResolvedValueOnce([
      { actorUserId: 'zm-1', actorRole: 'DD_ZM', decision: 'Approved' },
    ]);
    const result = await WorkflowService.evaluateStagePolicy('app-1', 'LEVEL_1');
    expect(result.policyMet).toBe(true);
  });

  it('TC-WESP-04: ALL mode — policyMet=false when only one of two required roles responded', async () => {
    const db = (await import('../database/models/index.js')).default as any;
    db.StageApprovalPolicy.findOne.mockResolvedValueOnce({
      requiredRoles: ['DD_ZM', 'RBM'],
      approvalMode: 'ALL',
      minApprovals: 2,
    });
    db.StageApprovalAction.findAll.mockResolvedValueOnce([
      { actorUserId: 'zm-1', actorRole: 'DD_ZM', decision: 'Approved' },
      // RBM has NOT responded yet
    ]);
    const result = await WorkflowService.evaluateStagePolicy('app-1', 'LEVEL_1');
    expect(result.policyMet).toBe(false);
  });

  it('TC-WESP-05: ALL mode — policyMet=true when both RBM and DD_ZM have responded', async () => {
    const db = (await import('../database/models/index.js')).default as any;
    db.StageApprovalPolicy.findOne.mockResolvedValueOnce({
      requiredRoles: ['DD_ZM', 'RBM'],
      approvalMode: 'ALL',
      minApprovals: 2,
    });
    db.StageApprovalAction.findAll.mockResolvedValueOnce([
      { actorUserId: 'zm-1', actorRole: 'DD_ZM', decision: 'Approved' },
      { actorUserId: 'rbm-1', actorRole: 'RBM', decision: 'Approved' },
    ]);
    const result = await WorkflowService.evaluateStagePolicy('app-1', 'LEVEL_1');
    expect(result.policyMet).toBe(true);
  });
});
