/**
 * Tests for src/workflows/wf.opportunity.ts
 *
 * Covers:
 * - Exported constants (OpportunityStatus, StatusIdToKey, OptimaType, WorkflowPermissions)
 * - Guard helpers (assertOptimaStage, assertNotTerminal, assertTransitionAllowed, assertNotePresent)
 * - Transition functions (transitionToNew, transitionToInternalReview, handleReviewDecision,
 *   transitionToQuoteSent, transitionToConfirmedQuote, finalizeOpportunity, transitionToPending,
 *   resurrectOpportunity, beginRevision, cancelOpportunity, reopenCancelledOpportunity,
 *   triggerColdDetection)
 * - Master dispatcher (processOpportunityAction)
 */

import { describe, test, expect, mock, beforeEach } from "bun:test";

// ---------------------------------------------------------------------------
// Mock dependencies before importing the workflow module
// ---------------------------------------------------------------------------

// Instead of mocking ActivityController directly (which contaminates the
// global module registry and breaks ActivityController.test.ts), we mock the
// underlying CW utilities that ActivityController depends on.  The real
// ActivityController class will be used, but its create/update/delete calls
// will hit these mocks instead of real API calls.

const mockCwActivityCreate = mock(() =>
  Promise.resolve({
    id: 9001,
    name: "Mock Activity",
    notes: null,
    type: { id: 3, name: "HistoricEntry" },
    status: { id: 2, name: "Closed" },
    company: null,
    contact: null,
    opportunity: { id: 1001, name: "Test Opp" },
    assignTo: { id: 10, name: "Test User", identifier: "tuser" },
    customFields: [],
    _info: {},
  }),
);

const mockCwActivityUpdate = mock((id: number, ops: any) =>
  Promise.resolve({
    id,
    name: "Mock Activity",
    notes: null,
    type: { id: 3, name: "HistoricEntry" },
    status: { id: 2, name: "Closed" },
    company: null,
    contact: null,
    opportunity: { id: 1001, name: "Test Opp" },
    assignTo: { id: 10, name: "Test User", identifier: "tuser" },
    customFields: [],
    _info: {},
  }),
);

const mockFetchByOpportunityDirect = mock(() => Promise.resolve([]));

mock.module("../../src/modules/cw-utils/activities/activities", () => ({
  activityCw: {
    create: mockCwActivityCreate,
    update: mockCwActivityUpdate,
    delete: mock(() => Promise.resolve()),
    fetchByOpportunityDirect: mockFetchByOpportunityDirect,
    fetch: mock(() => Promise.resolve({ id: 9001, name: "Mock", _info: {} })),
    fetchAll: mock(() => Promise.resolve(new Map())),
    fetchByCompany: mock(() => Promise.resolve(new Map())),
    fetchByOpportunity: mock(() => Promise.resolve(new Map())),
    fetchAllSummaries: mock(() => Promise.resolve(new Map())),
    countItems: mock(() => Promise.resolve(0)),
    replace: mock(() => Promise.resolve({ id: 9001 })),
  },
}));

// Also mock fetchActivity used by ActivityController.refreshFromCW
mock.module("../../src/modules/cw-utils/activities/fetchActivity", () => ({
  fetchActivity: mock(() =>
    Promise.resolve({ id: 9001, name: "Mock", _info: {} }),
  ),
}));

const mockSyncOpportunityStatus = mock(() => Promise.resolve());
const mockSubmitTimeEntry = mock(() => Promise.resolve());

mock.module("../../src/services/cw.opportunityService", () => ({
  syncOpportunityStatus: mockSyncOpportunityStatus,
  submitTimeEntry: mockSubmitTimeEntry,
}));

const REAL_COLD_THRESHOLDS: Record<number, { days: number; ms: number }> = {
  43: { days: 14, ms: 14 * 24 * 60 * 60 * 1000 },
  57: { days: 30, ms: 30 * 24 * 60 * 60 * 1000 },
};

/** checkColdStatus is bypassed in source — always returns not-cold. */
const mockCheckColdStatus = mock(() => ({
  cold: false as const,
  triggeredBy: null,
}));

mock.module("../../src/modules/algorithms/algo.coldThreshold", () => ({
  checkColdStatus: mockCheckColdStatus,
  COLD_THRESHOLDS: REAL_COLD_THRESHOLDS,
}));

// ---------------------------------------------------------------------------
// Import the module under test (after mocks are in place)
// ---------------------------------------------------------------------------

import {
  OpportunityStatus,
  StatusIdToKey,
  OptimaType,
  WorkflowPermissions,
  processOpportunityAction,
  transitionToNew,
  transitionToInternalReview,
  handleReviewDecision,
  transitionToQuoteSent,
  transitionToConfirmedQuote,
  finalizeOpportunity,
  transitionToPending,
  resurrectOpportunity,
  beginRevision,
  cancelOpportunity,
  reopenCancelledOpportunity,
  triggerColdDetection,
  type WorkflowUser,
  type WorkflowResult,
} from "../../src/workflows/wf.opportunity";

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function makeUser(overrides: Partial<WorkflowUser> = {}): WorkflowUser {
  return {
    id: "user-1",
    cwMemberId: 10,
    permissions: ["*"], // all permissions by default
    ...overrides,
  };
}

function makeOpportunity(overrides: Record<string, any> = {}): any {
  return {
    cwOpportunityId: 1001,
    companyCwId: 123,
    name: "Test Opportunity",
    statusCwId: OpportunityStatus.PendingNew,
    stageName: "Optima",
    refreshFromCW: mock(() => Promise.resolve()),
    ...overrides,
  };
}

beforeEach(() => {
  mockCwActivityCreate.mockClear();
  mockCwActivityCreate.mockImplementation(() =>
    Promise.resolve({
      id: 9001,
      name: "Mock Activity",
      notes: null,
      type: { id: 3, name: "HistoricEntry" },
      status: { id: 2, name: "Closed" },
      company: null,
      contact: null,
      opportunity: { id: 1001, name: "Test Opp" },
      assignTo: { id: 10, name: "Test User", identifier: "tuser" },
      customFields: [],
      _info: {},
    }),
  );

  mockCwActivityUpdate.mockClear();
  mockCwActivityUpdate.mockImplementation((id: number) =>
    Promise.resolve({
      id,
      name: "Mock Activity",
      notes: null,
      type: { id: 3, name: "HistoricEntry" },
      status: { id: 2, name: "Closed" },
      company: null,
      contact: null,
      opportunity: { id: 1001, name: "Test Opp" },
      assignTo: { id: 10, name: "Test User", identifier: "tuser" },
      customFields: [],
      _info: {},
    }),
  );

  mockFetchByOpportunityDirect.mockClear();
  mockFetchByOpportunityDirect.mockImplementation(() => Promise.resolve([]));

  mockSyncOpportunityStatus.mockClear();
  mockSyncOpportunityStatus.mockImplementation(() => Promise.resolve());

  mockSubmitTimeEntry.mockClear();
  mockSubmitTimeEntry.mockImplementation(() => Promise.resolve());

  mockCheckColdStatus.mockClear();
  mockCheckColdStatus.mockImplementation(() => ({
    cold: false,
    triggeredBy: null,
  }));
});

// ═══════════════════════════════════════════════════════════════════════════
// CONSTANTS
// ═══════════════════════════════════════════════════════════════════════════

describe("Exported constants", () => {
  test("OpportunityStatus has all expected keys", () => {
    expect(OpportunityStatus.PendingNew).toBe(37);
    expect(OpportunityStatus.New).toBe(24);
    expect(OpportunityStatus.InternalReview).toBe(56);
    expect(OpportunityStatus.QuoteSent).toBe(43);
    expect(OpportunityStatus.ConfirmedQuote).toBe(57);
    expect(OpportunityStatus.Active).toBe(58);
    expect(OpportunityStatus.PendingSent).toBe(60);
    expect(OpportunityStatus.PendingRevision).toBe(61);
    expect(OpportunityStatus.PendingWon).toBe(49);
    expect(OpportunityStatus.Won).toBe(29);
    expect(OpportunityStatus.PendingLost).toBe(50);
    expect(OpportunityStatus.Lost).toBe(53);
    expect(OpportunityStatus.Canceled).toBe(59);
  });

  test("StatusIdToKey reverses OpportunityStatus", () => {
    expect(StatusIdToKey[37]).toBe("PendingNew");
    expect(StatusIdToKey[24]).toBe("New");
    expect(StatusIdToKey[29]).toBe("Won");
    expect(StatusIdToKey[53]).toBe("Lost");
  });

  test("OptimaType has the expected field ID and values", () => {
    expect(OptimaType.FIELD_ID).toBe(45);
    expect(OptimaType.OpportunityCreated).toBe("Opportunity Created");
    expect(OptimaType.QuoteSent).toBe("Quote Sent");
    expect(OptimaType.Converted).toBe("Converted");
  });

  test("WorkflowPermissions are namespaced correctly", () => {
    expect(WorkflowPermissions.FINALIZE).toBe("sales.opportunity.finalize");
    expect(WorkflowPermissions.CANCEL).toBe("sales.opportunity.cancel");
    expect(WorkflowPermissions.REVIEW).toBe("sales.opportunity.review");
    expect(WorkflowPermissions.SEND).toBe("sales.opportunity.send");
    expect(WorkflowPermissions.REOPEN).toBe("sales.opportunity.reopen");
    expect(WorkflowPermissions.WIN).toBe("sales.opportunity.win");
    expect(WorkflowPermissions.LOSE).toBe("sales.opportunity.lose");
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// TRANSITION: PendingNew → New
// ═══════════════════════════════════════════════════════════════════════════

describe("transitionToNew", () => {
  test("succeeds when status is PendingNew", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingNew });
    const user = makeUser();

    const result = await transitionToNew(opp, user, {});
    expect(result.success).toBe(true);
    expect(result.previousStatusId).toBe(OpportunityStatus.PendingNew);
    expect(result.newStatusId).toBe(OpportunityStatus.New);
    expect(result.activitiesCreated).toHaveLength(1);
    expect(mockSyncOpportunityStatus).toHaveBeenCalledTimes(1);
  });

  test("fails when status is not PendingNew", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.Active });
    const result = await transitionToNew(opp, makeUser(), {});
    expect(result.success).toBe(false);
    expect(result.error).toBeTruthy();
  });

  test("defaults to PendingNew when statusCwId is null", async () => {
    const opp = makeOpportunity({ statusCwId: null });
    const result = await transitionToNew(opp, makeUser(), {});
    // transitionToNew defaults null → PendingNew, so transition succeeds
    expect(result.success).toBe(true);
    expect(result.previousStatusId).toBe(OpportunityStatus.PendingNew);
    expect(result.newStatusId).toBe(OpportunityStatus.New);
  });

  test("submits time entry when timeStarted/timeEnded provided", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingNew });
    await transitionToNew(opp, makeUser(), {
      timeStarted: "2026-03-01T09:00:00Z",
      timeEnded: "2026-03-01T10:00:00Z",
    });
    expect(mockSubmitTimeEntry).toHaveBeenCalledTimes(1);
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// TRANSITION: → InternalReview
// ═══════════════════════════════════════════════════════════════════════════

describe("transitionToInternalReview", () => {
  test("succeeds from New with note", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.New });
    const result = await transitionToInternalReview(opp, makeUser(), {
      note: "Needs review",
    });
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.InternalReview);
  });

  test("requires REVIEW permission", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.New });
    const user = makeUser({ permissions: [] });
    const result = await transitionToInternalReview(opp, user, {
      note: "review",
    });
    expect(result.success).toBe(false);
    expect(result.error).toContain(WorkflowPermissions.REVIEW);
  });

  test("requires a non-empty note", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.New });
    const result = await transitionToInternalReview(opp, makeUser(), {
      note: "",
    });
    expect(result.success).toBe(false);
    expect(result.error).toBeTruthy();
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// REVIEW DECISION
// ═══════════════════════════════════════════════════════════════════════════

describe("handleReviewDecision", () => {
  test("approve → ReadyToSend", async () => {
    const opp = makeOpportunity({
      statusCwId: OpportunityStatus.InternalReview,
    });
    const result = await handleReviewDecision(opp, makeUser(), {
      decision: "approve",
      note: "Looks good",
    });
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.ReadyToSend);
  });

  test("reject → PendingRevision", async () => {
    const opp = makeOpportunity({
      statusCwId: OpportunityStatus.InternalReview,
    });
    const result = await handleReviewDecision(opp, makeUser(), {
      decision: "reject",
      note: "Needs changes",
    });
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.PendingRevision);
  });

  test("send → QuoteSent", async () => {
    const opp = makeOpportunity({
      statusCwId: OpportunityStatus.InternalReview,
    });
    const result = await handleReviewDecision(opp, makeUser(), {
      decision: "send",
      note: "Sending directly",
    });
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.QuoteSent);
    // "send" creates TWO activities (approved + sent)
    expect(result.activitiesCreated).toHaveLength(2);
  });

  test("cancel → Canceled (requires CANCEL permission)", async () => {
    const opp = makeOpportunity({
      statusCwId: OpportunityStatus.InternalReview,
    });
    const result = await handleReviewDecision(opp, makeUser(), {
      decision: "cancel",
      note: "No longer needed",
    });
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.Canceled);
  });

  test("cancel fails without CANCEL permission", async () => {
    const opp = makeOpportunity({
      statusCwId: OpportunityStatus.InternalReview,
    });
    const user = makeUser({
      permissions: [WorkflowPermissions.REVIEW], // no CANCEL
    });
    const result = await handleReviewDecision(opp, user, {
      decision: "cancel",
      note: "No longer needed",
    });
    expect(result.success).toBe(false);
    expect(result.error).toContain(WorkflowPermissions.CANCEL);
  });

  test("fails when status is not InternalReview", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.New });
    const result = await handleReviewDecision(opp, makeUser(), {
      decision: "approve",
      note: "ok",
    });
    expect(result.success).toBe(false);
    expect(result.error).toContain("InternalReview");
  });

  test("requires note", async () => {
    const opp = makeOpportunity({
      statusCwId: OpportunityStatus.InternalReview,
    });
    const result = await handleReviewDecision(opp, makeUser(), {
      decision: "approve",
      note: "",
    });
    expect(result.success).toBe(false);
  });

  test("unknown decision returns failure", async () => {
    const opp = makeOpportunity({
      statusCwId: OpportunityStatus.InternalReview,
    });
    const result = await handleReviewDecision(opp, makeUser(), {
      decision: "unknown" as any,
      note: "Something",
    });
    expect(result.success).toBe(false);
    expect(result.error).toContain("Unknown review decision");
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// TRANSITION: → QuoteSent (with compound flags)
// ═══════════════════════════════════════════════════════════════════════════

describe("transitionToQuoteSent", () => {
  test("plain send from PendingSent → QuoteSent", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingSent });
    const result = await transitionToQuoteSent(opp, makeUser(), {});
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.QuoteSent);
  });

  test("requires SEND permission", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingSent });
    const user = makeUser({ permissions: [] });
    const result = await transitionToQuoteSent(opp, user, {});
    expect(result.success).toBe(false);
    expect(result.error).toContain(WorkflowPermissions.SEND);
  });

  test("won flag → PendingWon (without finalize perm)", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingSent });
    const user = makeUser({
      permissions: [WorkflowPermissions.SEND, WorkflowPermissions.WIN],
    });
    const result = await transitionToQuoteSent(opp, user, { won: true });
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.PendingWon);
  });

  test("won + finalize → Won (with finalize perm)", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingSent });
    const user = makeUser(); // wildcard perms
    const result = await transitionToQuoteSent(opp, user, {
      won: true,
      finalize: true,
    });
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.Won);
    expect(result.activitiesCreated.length).toBeGreaterThanOrEqual(2);
  });

  test("lost flag → PendingLost (without finalize perm)", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingSent });
    const user = makeUser({
      permissions: [WorkflowPermissions.SEND, WorkflowPermissions.LOSE],
    });
    const result = await transitionToQuoteSent(opp, user, { lost: true });
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.PendingLost);
  });

  test("lost + finalize → Lost (with finalize perm)", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingSent });
    const user = makeUser();
    const result = await transitionToQuoteSent(opp, user, {
      lost: true,
      finalize: true,
    });
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.Lost);
  });

  test("needsRevision → Active", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingSent });
    const result = await transitionToQuoteSent(opp, makeUser(), {
      needsRevision: true,
    });
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.Active);
    expect(result.activitiesCreated).toHaveLength(2);
  });

  test("quoteConfirmed → ConfirmedQuote", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingSent });
    const result = await transitionToQuoteSent(opp, makeUser(), {
      quoteConfirmed: true,
    });
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.ConfirmedQuote);
  });

  test("won flag without WIN perm fails", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingSent });
    const user = makeUser({ permissions: [WorkflowPermissions.SEND] });
    const result = await transitionToQuoteSent(opp, user, { won: true });
    expect(result.success).toBe(false);
    expect(result.error).toContain(WorkflowPermissions.WIN);
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// TRANSITION: → ConfirmedQuote
// ═══════════════════════════════════════════════════════════════════════════

describe("transitionToConfirmedQuote", () => {
  test("succeeds from QuoteSent", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.QuoteSent });
    const result = await transitionToConfirmedQuote(opp, makeUser(), {});
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.ConfirmedQuote);
  });

  test("fails from disallowed status", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.New });
    const result = await transitionToConfirmedQuote(opp, makeUser(), {});
    expect(result.success).toBe(false);
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// FINALIZE
// ═══════════════════════════════════════════════════════════════════════════

describe("finalizeOpportunity", () => {
  test("won from PendingWon → Won", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingWon });
    const result = await finalizeOpportunity(opp, makeUser(), {
      outcome: "won",
      note: "Deal closed",
    });
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.Won);
  });

  test("lost from PendingLost → Lost", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingLost });
    const result = await finalizeOpportunity(opp, makeUser(), {
      outcome: "lost",
      note: "Customer chose competitor",
    });
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.Lost);
  });

  test("requires FINALIZE permission", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingWon });
    const user = makeUser({ permissions: [WorkflowPermissions.WIN] });
    const result = await finalizeOpportunity(opp, user, {
      outcome: "won",
      note: "Close it",
    });
    expect(result.success).toBe(false);
    expect(result.error).toContain(WorkflowPermissions.FINALIZE);
  });

  test("requires a note", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingWon });
    const result = await finalizeOpportunity(opp, makeUser(), {
      outcome: "won",
      note: "",
    });
    expect(result.success).toBe(false);
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// TRANSITION TO PENDING (Win/Lose without finalize perm)
// ═══════════════════════════════════════════════════════════════════════════

describe("transitionToPending", () => {
  test("won from QuoteSent → PendingWon", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.QuoteSent });
    const result = await transitionToPending(opp, makeUser(), {
      outcome: "won",
      note: "Customer accepted",
    });
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.PendingWon);
  });

  test("lost from ConfirmedQuote → PendingLost", async () => {
    const opp = makeOpportunity({
      statusCwId: OpportunityStatus.ConfirmedQuote,
    });
    const result = await transitionToPending(opp, makeUser(), {
      outcome: "lost",
      note: "Declined",
    });
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.PendingLost);
  });

  test("requires WIN permission for won outcome", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.QuoteSent });
    const user = makeUser({ permissions: [] });
    const result = await transitionToPending(opp, user, {
      outcome: "won",
      note: "Accepted",
    });
    expect(result.success).toBe(false);
    expect(result.error).toContain(WorkflowPermissions.WIN);
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// RESURRECT
// ═══════════════════════════════════════════════════════════════════════════

describe("resurrectOpportunity", () => {
  test("PendingLost → Active", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingLost });
    const result = await resurrectOpportunity(opp, makeUser(), {
      note: "Reconsider",
    });
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.Active);
  });

  test("PendingWon → Active requires FINALIZE perm", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingWon });
    const user = makeUser({ permissions: [] });
    const result = await resurrectOpportunity(opp, user, {
      note: "Revise",
    });
    expect(result.success).toBe(false);
    expect(result.error).toContain("permission");
  });

  test("requires note", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingLost });
    const result = await resurrectOpportunity(opp, makeUser(), { note: "" });
    expect(result.success).toBe(false);
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// BEGIN REVISION
// ═══════════════════════════════════════════════════════════════════════════

describe("beginRevision", () => {
  test("PendingRevision → Active", async () => {
    const opp = makeOpportunity({
      statusCwId: OpportunityStatus.PendingRevision,
    });
    const result = await beginRevision(opp, makeUser(), {});
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.Active);
  });

  test("fails from wrong status", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.New });
    const result = await beginRevision(opp, makeUser(), {});
    expect(result.success).toBe(false);
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// CANCEL
// ═══════════════════════════════════════════════════════════════════════════

describe("cancelOpportunity", () => {
  test("New → Canceled", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.New });
    const result = await cancelOpportunity(opp, makeUser(), {
      note: "No longer needed",
    });
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.Canceled);
  });

  test("requires CANCEL permission", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.New });
    const user = makeUser({ permissions: [] });
    const result = await cancelOpportunity(opp, user, {
      note: "Cancel",
    });
    expect(result.success).toBe(false);
    expect(result.error).toContain(WorkflowPermissions.CANCEL);
  });

  test("requires note", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.New });
    const result = await cancelOpportunity(opp, makeUser(), { note: "" });
    expect(result.success).toBe(false);
  });

  test("fails from terminal status", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.Won });
    const result = await cancelOpportunity(opp, makeUser(), {
      note: "Can't cancel Won",
    });
    expect(result.success).toBe(false);
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// REOPEN
// ═══════════════════════════════════════════════════════════════════════════

describe("reopenCancelledOpportunity", () => {
  test("Canceled → Active", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.Canceled });
    const result = await reopenCancelledOpportunity(opp, makeUser(), {
      note: "Reopening for updated scope",
    });
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.Active);
  });

  test("requires REOPEN permission", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.Canceled });
    const user = makeUser({ permissions: [] });
    const result = await reopenCancelledOpportunity(opp, user, {
      note: "Reopen",
    });
    expect(result.success).toBe(false);
    expect(result.error).toContain(WorkflowPermissions.REOPEN);
  });

  test("requires note", async () => {
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.Canceled });
    const result = await reopenCancelledOpportunity(opp, makeUser(), {
      note: "",
    });
    expect(result.success).toBe(false);
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// COLD DETECTION
// ═══════════════════════════════════════════════════════════════════════════

describe("triggerColdDetection", () => {
  test("returns success with no status change when not cold", async () => {
    mockCheckColdStatus.mockReturnValue({ cold: false, triggeredBy: null });
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.QuoteSent });

    const result = await triggerColdDetection(opp, new Date("2026-03-01"));
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.QuoteSent);
    expect(result.activitiesCreated).toHaveLength(0);
    expect(mockSyncOpportunityStatus).not.toHaveBeenCalled();
  });

  test("transitions to InternalReview when cold", async () => {
    mockCheckColdStatus.mockReturnValue({
      cold: true,
      triggeredBy: {
        statusCwId: 43,
        statusName: "QuoteSent",
        thresholdDays: 14,
        staleDays: 20,
      },
    });
    const opp = makeOpportunity({ statusCwId: OpportunityStatus.QuoteSent });

    const result = await triggerColdDetection(opp, new Date("2026-01-01"));
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.InternalReview);
    expect(result.coldCheck?.cold).toBe(true);
    expect(result.activitiesCreated).toHaveLength(1);
    expect(mockSyncOpportunityStatus).toHaveBeenCalledTimes(1);
  });

  test("fails when statusCwId is null", async () => {
    const opp = makeOpportunity({ statusCwId: null });
    const result = await triggerColdDetection(opp, null);
    expect(result.success).toBe(false);
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// MASTER DISPATCHER
// ═══════════════════════════════════════════════════════════════════════════

describe("processOpportunityAction", () => {
  test("rejects non-Optima stage", async () => {
    const opp = makeOpportunity({ stageName: "Pipeline" });
    const result = await processOpportunityAction(
      opp,
      { action: "acceptNew", payload: {} },
      makeUser(),
    );
    expect(result.success).toBe(false);
    expect(result.error).toBeTruthy();
  });

  test("rejects terminal status for non-reopen actions", async () => {
    const opp = makeOpportunity({
      statusCwId: OpportunityStatus.Won,
      stageName: "Optima",
    });
    const result = await processOpportunityAction(
      opp,
      { action: "acceptNew", payload: {} },
      makeUser(),
    );
    expect(result.success).toBe(false);
  });

  test("routes acceptNew correctly", async () => {
    const opp = makeOpportunity({
      statusCwId: OpportunityStatus.PendingNew,
      stageName: "Optima",
    });
    const result = await processOpportunityAction(
      opp,
      { action: "acceptNew", payload: {} },
      makeUser(),
    );
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.New);
  });

  test("calls refreshFromCW on success", async () => {
    const refreshFn = mock(() => Promise.resolve());
    const opp = makeOpportunity({
      statusCwId: OpportunityStatus.PendingNew,
      stageName: "Optima",
      refreshFromCW: refreshFn,
    });
    await processOpportunityAction(
      opp,
      { action: "acceptNew", payload: {} },
      makeUser(),
    );
    expect(refreshFn).toHaveBeenCalledTimes(1);
  });

  test("does not call refreshFromCW on failure", async () => {
    const refreshFn = mock(() => Promise.resolve());
    const opp = makeOpportunity({
      statusCwId: OpportunityStatus.Active,
      stageName: "Optima",
      refreshFromCW: refreshFn,
    });
    await processOpportunityAction(
      opp,
      { action: "acceptNew", payload: {} },
      makeUser(),
    );
    expect(refreshFn).not.toHaveBeenCalled();
  });

  test("routes finalize to finalizeOpportunity with FINALIZE perm", async () => {
    const opp = makeOpportunity({
      statusCwId: OpportunityStatus.PendingWon,
      stageName: "Optima",
    });
    const result = await processOpportunityAction(
      opp,
      { action: "finalize", payload: { outcome: "won", note: "Done" } },
      makeUser(),
    );
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.Won);
  });

  test("routes finalize to transitionToPending without FINALIZE perm", async () => {
    const opp = makeOpportunity({
      statusCwId: OpportunityStatus.QuoteSent,
      stageName: "Optima",
    });
    const user = makeUser({
      permissions: [WorkflowPermissions.WIN],
    });
    const result = await processOpportunityAction(
      opp,
      { action: "finalize", payload: { outcome: "won", note: "Accepted" } },
      user,
    );
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.PendingWon);
  });

  test("reopen allowed from Canceled (skips terminal check)", async () => {
    const opp = makeOpportunity({
      statusCwId: OpportunityStatus.Canceled,
      stageName: "Optima",
    });
    const result = await processOpportunityAction(
      opp,
      { action: "reopen", payload: { note: "Reopening" } },
      makeUser(),
    );
    expect(result.success).toBe(true);
    expect(result.newStatusId).toBe(OpportunityStatus.Active);
  });

  test("closes open workflow activities before transitioning", async () => {
    const opp = makeOpportunity({
      statusCwId: OpportunityStatus.PendingNew,
      stageName: "Optima",
    });
    await processOpportunityAction(
      opp,
      { action: "acceptNew", payload: {} },
      makeUser(),
    );
    // closeOpenWorkflowActivities calls fetchByOpportunityDirect
    expect(mockFetchByOpportunityDirect).toHaveBeenCalledTimes(1);
  });

  test("refreshFromCW failure does not fail the workflow", async () => {
    const opp = makeOpportunity({
      statusCwId: OpportunityStatus.PendingNew,
      stageName: "Optima",
      refreshFromCW: mock(() => Promise.reject(new Error("CW down"))),
    });
    const result = await processOpportunityAction(
      opp,
      { action: "acceptNew", payload: {} },
      makeUser(),
    );
    // Transition itself should still succeed
    expect(result.success).toBe(true);
  });
});
