import { describe, test, expect, mock, beforeEach } from "bun:test";
import {
  buildMockOpportunity,
  buildMockCompany,
  buildMockConstants,
} from "../setup";

// ---------------------------------------------------------------------------
// Stable mock factory
// ---------------------------------------------------------------------------

function createStablePrismaMock(
  overrides: Record<string, Record<string, any>> = {},
) {
  return new Proxy(
    {},
    {
      get(_target, model: string) {
        if (model === "$connect" || model === "$disconnect")
          return mock(() => Promise.resolve());
        if (overrides[model]) return overrides[model];
        return new Proxy({}, { get: () => mock(() => Promise.resolve(null)) });
      },
    },
  );
}

/**
 * Build a complete cache mock with explicit named exports.
 *
 * Uses concrete properties instead of a Proxy so that Bun's ESM mock
 * resolution can discover every named export at module-link time
 * (some Bun versions do not enumerate Proxy keys for static imports).
 */
function buildCacheMock(overrides: Record<string, any> = {}) {
  return {
    // Key helpers — use real prefixes so cross-file mock leaks don't
    // break opportunityCache.test.ts key assertions.
    activityCacheKey: mock((id: number) => `opp:activities:${id}`),
    companyCwCacheKey: mock((id: number) => `opp:company-cw:${id}`),
    notesCacheKey: mock((id: number) => `opp:notes:${id}`),
    contactsCacheKey: mock((id: number) => `opp:contacts:${id}`),
    productsCacheKey: mock((id: number) => `opp:products:${id}`),
    siteCacheKey: mock((a: number, b: number) => `opp:site:${a}:${b}`),
    oppCwDataCacheKey: mock((id: number) => `opp:cw-data:${id}`),
    // Read helpers
    getCachedActivities: mock(() => Promise.resolve(null)),
    getCachedCompanyCwData: mock(() => Promise.resolve(null)),
    getCachedNotes: mock(() => Promise.resolve(null)),
    getCachedContacts: mock(() => Promise.resolve(null)),
    getCachedProducts: mock(() => Promise.resolve(null)),
    getCachedSite: mock(() => Promise.resolve(null)),
    getCachedOppCwData: mock(() => Promise.resolve(null)),
    // Write / fetch helpers
    fetchAndCacheActivities: mock(() => Promise.resolve(null)),
    fetchAndCacheCompanyCwData: mock(() => Promise.resolve(null)),
    fetchAndCacheNotes: mock(() => Promise.resolve(null)),
    fetchAndCacheContacts: mock(() => Promise.resolve(null)),
    fetchAndCacheProducts: mock(() => Promise.resolve(null)),
    fetchAndCacheSite: mock(() => Promise.resolve(null)),
    fetchAndCacheOppCwData: mock(() => Promise.resolve(null)),
    // Invalidation helpers
    invalidateNotesCache: mock(() => Promise.resolve()),
    invalidateContactsCache: mock(() => Promise.resolve()),
    invalidateProductsCache: mock(() => Promise.resolve()),
    invalidateAllOpportunityCaches: mock(() => Promise.resolve()),
    // Background refresh
    refreshOpportunityCache: mock(() => Promise.resolve()),
    ...overrides,
  };
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("opportunities manager", () => {
  beforeEach(() => {
    mock.restore();
  });

  // -------------------------------------------------------------------
  // fetchRecord (lightweight)
  // -------------------------------------------------------------------
  describe("fetchRecord()", () => {
    test("returns OpportunityController by internal ID", async () => {
      const oppData = {
        ...buildMockOpportunity(),
        company: buildMockCompany(),
      };
      mock.module("../../src/constants", () =>
        buildMockConstants({
          prisma: createStablePrismaMock({
            opportunity: {
              findFirst: mock(() => Promise.resolve(oppData)),
            },
          }),
          redis: {
            get: mock(() => Promise.resolve(null)),
            set: mock(() => Promise.resolve("OK")),
            del: mock(() => Promise.resolve(1)),
          },
          connectWiseApi: {
            get: mock(() => Promise.resolve({ data: {} })),
          },
        }),
      );
      mock.module(
        "../../src/modules/cw-utils/opportunities/opportunities",
        () => ({
          opportunityCw: {
            fetch: mock(() => Promise.resolve(null)),
            create: mock(() => Promise.resolve({})),
            delete: mock(() => Promise.resolve()),
          },
        }),
      );
      mock.module("../../src/modules/cw-utils/activities/activities", () => ({
        activityCw: {
          fetchByOpportunityDirect: mock(() => Promise.resolve([])),
        },
      }));

      const { opportunities } =
        await import("../../src/managers/opportunities");
      const result = await opportunities.fetchRecord("opp-1");
      expect(result).toBeDefined();
    });

    test("throws 404 when opportunity not found", async () => {
      mock.module("../../src/constants", () =>
        buildMockConstants({
          prisma: createStablePrismaMock({
            opportunity: {
              findFirst: mock(() => Promise.resolve(null)),
            },
          }),
          redis: {
            get: mock(() => Promise.resolve(null)),
            set: mock(() => Promise.resolve("OK")),
            del: mock(() => Promise.resolve(1)),
          },
          connectWiseApi: {
            get: mock(() => Promise.resolve({ data: {} })),
          },
        }),
      );
      mock.module(
        "../../src/modules/cw-utils/opportunities/opportunities",
        () => ({
          opportunityCw: {
            fetch: mock(() => Promise.resolve(null)),
          },
        }),
      );
      mock.module("../../src/modules/cw-utils/activities/activities", () => ({
        activityCw: {
          fetchByOpportunityDirect: mock(() => Promise.resolve([])),
        },
      }));

      const { opportunities } =
        await import("../../src/managers/opportunities");
      try {
        await opportunities.fetchRecord("nonexistent");
        expect(true).toBe(false);
      } catch (e: any) {
        expect(e.name).toBe("OpportunityNotFound");
        expect(e.status).toBe(404);
      }
    });

    test("uses numeric identifier as cwOpportunityId", async () => {
      const oppData = { ...buildMockOpportunity(), company: null };
      const findFirst = mock(() => Promise.resolve(oppData));
      mock.module("../../src/constants", () =>
        buildMockConstants({
          prisma: createStablePrismaMock({
            opportunity: { findFirst },
          }),
          redis: {
            get: mock(() => Promise.resolve(null)),
            set: mock(() => Promise.resolve("OK")),
            del: mock(() => Promise.resolve(1)),
          },
          connectWiseApi: {
            get: mock(() => Promise.resolve({ data: {} })),
          },
        }),
      );
      mock.module(
        "../../src/modules/cw-utils/opportunities/opportunities",
        () => ({
          opportunityCw: {
            fetch: mock(() => Promise.resolve(null)),
          },
        }),
      );
      mock.module("../../src/modules/cw-utils/activities/activities", () => ({
        activityCw: {
          fetchByOpportunityDirect: mock(() => Promise.resolve([])),
        },
      }));

      const { opportunities } =
        await import("../../src/managers/opportunities");
      await opportunities.fetchRecord(1001);
      const where = findFirst.mock.calls[0]?.[0]?.where;
      expect(where).toHaveProperty("cwOpportunityId", 1001);
    });
  });

  // -------------------------------------------------------------------
  // count
  // -------------------------------------------------------------------
  describe("count()", () => {
    test("returns total count", async () => {
      const countMock = mock(() => Promise.resolve(15));
      mock.module("../../src/constants", () =>
        buildMockConstants({
          prisma: createStablePrismaMock({
            opportunity: {
              countMock,
              count: countMock,
              findFirst: mock(() => Promise.resolve(null)),
            },
          }),
          redis: {
            get: mock(() => Promise.resolve(null)),
            set: mock(() => Promise.resolve("OK")),
            del: mock(() => Promise.resolve(1)),
          },
          connectWiseApi: {
            get: mock(() => Promise.resolve({ data: {} })),
          },
        }),
      );
      mock.module(
        "../../src/modules/cw-utils/opportunities/opportunities",
        () => ({
          opportunityCw: {},
        }),
      );
      mock.module("../../src/modules/cw-utils/activities/activities", () => ({
        activityCw: {},
      }));

      const { opportunities } =
        await import("../../src/managers/opportunities");
      const result = await opportunities.count();
      expect(result).toBe(15);
    });

    test("counts only open when openOnly is true", async () => {
      const countMock = mock(() => Promise.resolve(8));
      mock.module("../../src/constants", () =>
        buildMockConstants({
          prisma: createStablePrismaMock({
            opportunity: {
              count: countMock,
              findFirst: mock(() => Promise.resolve(null)),
            },
          }),
          redis: {
            get: mock(() => Promise.resolve(null)),
            set: mock(() => Promise.resolve("OK")),
            del: mock(() => Promise.resolve(1)),
          },
          connectWiseApi: {
            get: mock(() => Promise.resolve({ data: {} })),
          },
        }),
      );
      mock.module(
        "../../src/modules/cw-utils/opportunities/opportunities",
        () => ({
          opportunityCw: {},
        }),
      );
      mock.module("../../src/modules/cw-utils/activities/activities", () => ({
        activityCw: {},
      }));

      const { opportunities } =
        await import("../../src/managers/opportunities");
      const result = await opportunities.count({ openOnly: true });
      expect(result).toBe(8);
    });
  });

  // -------------------------------------------------------------------
  // fetchPages
  // -------------------------------------------------------------------
  describe("fetchPages()", () => {
    test("returns paginated opportunity controllers", async () => {
      const items = [
        { ...buildMockOpportunity(), company: buildMockCompany() },
      ];
      mock.module("../../src/constants", () =>
        buildMockConstants({
          prisma: createStablePrismaMock({
            opportunity: {
              findMany: mock(() => Promise.resolve(items)),
              findFirst: mock(() => Promise.resolve(null)),
            },
          }),
          redis: {
            get: mock(() => Promise.resolve(null)),
            set: mock(() => Promise.resolve("OK")),
            del: mock(() => Promise.resolve(1)),
          },
          connectWiseApi: {
            get: mock(() => Promise.resolve({ data: {} })),
          },
        }),
      );
      mock.module(
        "../../src/modules/cw-utils/opportunities/opportunities",
        () => ({
          opportunityCw: {},
        }),
      );
      mock.module("../../src/modules/cw-utils/activities/activities", () => ({
        activityCw: {
          fetchByOpportunityDirect: mock(() => Promise.resolve([])),
        },
      }));

      const { opportunities } =
        await import("../../src/managers/opportunities");
      const result = await opportunities.fetchPages(1, 10);
      expect(result).toBeArrayOfSize(1);
    });
  });
});
