/**
 * Unit tests for the approvals POST handler.
 *
 * Focuses on HubSpot sync behaviour: after approve/unapprove changes are
 * persisted to the DB, the handler must push both the audit log
 * (client_measures_approval_log) and the structured field (approved_measures)
 * to HubSpot. Prior to this fix only the audit log was synced.
 */
import { describe, it, expect, vi, beforeEach } from "vitest";
import { NextRequest } from "next/server";

// ── Hoisted mocks (declared before vi.mock factories run) ─────────────────────
const {
  mockGetServerSession,
  syncMeasureApprovalsToHubSpotMock,
  syncMeasuresFieldToHubSpotMock,
  mockDbSelect,
  mockDbInsert,
} = vi.hoisted(() => ({
  mockGetServerSession: vi.fn(),
  syncMeasureApprovalsToHubSpotMock: vi.fn(),
  syncMeasuresFieldToHubSpotMock: vi.fn(),
  mockDbSelect: vi.fn(),
  mockDbInsert: vi.fn(),
}));

// ── Auth ──────────────────────────────────────────────────────────────────────
vi.mock("next-auth", () => ({ getServerSession: mockGetServerSession }));
vi.mock("@/app/api/auth/[...nextauth]/authOptions", () => ({
  AuthOptions: {},
}));

// ── HubSpot syncs ─────────────────────────────────────────────────────────────
vi.mock("@/app/lib/hubspot/dealSync", () => ({
  syncMeasureApprovalsToHubSpot: syncMeasureApprovalsToHubSpotMock,
  syncMeasuresFieldToHubSpot: syncMeasuresFieldToHubSpotMock,
}));
vi.mock("@/app/lib/instructMeasure", () => ({
  APPROVED_MEASURES_PROP: "approved_measures",
}));

// ── Drizzle ORM ───────────────────────────────────────────────────────────────
vi.mock("drizzle-orm", () => ({
  and: vi.fn((...args: unknown[]) => ({ $and: args })),
  eq: vi.fn((a: unknown, b: unknown) => ({ $eq: [a, b] })),
  inArray: vi.fn((col: unknown, vals: unknown) => ({ $inArray: [col, vals] })),
  sql: vi.fn(),
}));

// ── DB schema stubs ───────────────────────────────────────────────────────────
vi.mock("@/app/db/schema/approvals", () => ({
  dealMeasureApprovals: { hubspotDealId: {}, measureName: {}, isApproved: {}, approvedBy: {}, approvedAt: {} },
  dealMeasureApprovalEvents: { hubspotDealId: {}, measureName: {}, action: {}, actedBy: {}, actedAt: {} },
}));
vi.mock("@/app/db/schema/portfolio", () => ({
  portfolioCapabilities: { portfolioId: {}, userId: {}, capability: {}, id: {} },
}));
vi.mock("@/app/db/schema/users", () => ({
  user: { id: {}, email: {}, firstName: {} },
}));

// ── DB mock ───────────────────────────────────────────────────────────────────
vi.mock("@/app/db/db", () => ({
  db: {
    get select() { return mockDbSelect; },
    get insert() { return mockDbInsert; },
  },
}));

// ── DB mock helpers ────────────────────────────────────────────────────────────
// Builds a thenable select chain where .limit() resolves to `limitResult`
// and awaiting the chain without .limit() resolves to `directResult`.
function makeSelectChain(
  limitResult: unknown[],
  directResult: unknown[] = [],
) {
  const self: Record<string, unknown> = {};
  // thenable so `await chain.where(...)` resolves to directResult
  self["then"] = (
    resolve: (v: unknown) => unknown,
    reject: (e: unknown) => unknown,
  ) => Promise.resolve(directResult).then(resolve, reject);
  self["from"] = vi.fn(() => self);
  self["leftJoin"] = vi.fn(() => self);
  self["where"] = vi.fn(() => self);
  self["limit"] = vi.fn(() => Promise.resolve(limitResult));
  return self;
}

// Builds an insert chain. .values() is thenable (plain insert) and also
// exposes .onConflictDoUpdate() (upsert).
function makeInsertChain() {
  const values: Record<string, unknown> = {};
  values["then"] = (
    resolve: (v: unknown) => unknown,
    reject: (e: unknown) => unknown,
  ) => Promise.resolve(undefined).then(resolve, reject);
  values["onConflictDoUpdate"] = vi.fn(() => Promise.resolve(undefined));
  const insert = { values: vi.fn(() => values) };
  return insert;
}


// ── Subject under test ────────────────────────────────────────────────────────
import { POST } from "./route";

// ── Helpers ───────────────────────────────────────────────────────────────────
function makeRequest(body: unknown, portfolioId = "10") {
  const req = new NextRequest(
    `http://localhost/api/portfolio/${portfolioId}/approvals`,
    {
      method: "POST",
      body: JSON.stringify(body),
      headers: { "content-type": "application/json" },
    },
  );
  return { req, params: Promise.resolve({ portfolioId }) };
}

function setupHappyPath(approvalRowsAfterChange: Array<{ measureName: string; approvedByEmail: string }>) {
  // 1. getServerSession
  mockGetServerSession.mockResolvedValue({ user: { email: "approver@test.com" } });

  // 2. getUserId select
  mockDbSelect.mockImplementationOnce(() =>
    makeSelectChain([{ id: 1n }]),
  );

  // 3. hasApproverCapability select
  mockDbSelect.mockImplementationOnce(() =>
    makeSelectChain([{ id: 1n }]),
  );

  // 4. upsert dealMeasureApprovals (one per change)
  mockDbInsert.mockImplementationOnce(() => makeInsertChain());

  // 5. insert dealMeasureApprovalEvents (one per change)
  mockDbInsert.mockImplementationOnce(() => makeInsertChain());

  // 6. post-change approvalRows select (no .limit — awaited at .where())
  mockDbSelect.mockImplementationOnce(() =>
    makeSelectChain([], approvalRowsAfterChange),
  );

  // HubSpot syncs
  syncMeasureApprovalsToHubSpotMock.mockResolvedValue(undefined);
  syncMeasuresFieldToHubSpotMock.mockResolvedValue({ ok: true });
}

// ── Tests ─────────────────────────────────────────────────────────────────────
describe("POST /approvals — approved_measures HubSpot sync", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("syncs approved_measures field to HubSpot after an unapprove action", async () => {
    setupHappyPath([
      // Only one measure remains approved after the unapprove
      { measureName: "ASHP", approvedByEmail: "approver@test.com" },
    ]);

    const { req, params } = makeRequest({
      changes: [
        { hubspotDealId: "deal-1", measureName: "Solar PV", approved: false },
      ],
    });

    const res = await POST(req, { params });
    expect(res.status).toBe(200);

    // Allow fire-and-forget promises to settle
    await vi.waitFor(() =>
      expect(syncMeasuresFieldToHubSpotMock).toHaveBeenCalled(),
    );

    expect(syncMeasuresFieldToHubSpotMock).toHaveBeenCalledWith({
      hubspotDealId: "deal-1",
      propName: "approved_measures",
      measureNames: ["ASHP"],
    });
  });

  it("syncs approved_measures with empty list when all measures removed", async () => {
    setupHappyPath([]); // nothing approved after removal

    const { req, params } = makeRequest({
      changes: [
        { hubspotDealId: "deal-2", measureName: "ASHP", approved: false },
      ],
    });

    const res = await POST(req, { params });
    expect(res.status).toBe(200);

    await vi.waitFor(() =>
      expect(syncMeasuresFieldToHubSpotMock).toHaveBeenCalled(),
    );

    expect(syncMeasuresFieldToHubSpotMock).toHaveBeenCalledWith({
      hubspotDealId: "deal-2",
      propName: "approved_measures",
      measureNames: [],
    });
  });

  it("syncs approved_measures when a new measure is approved", async () => {
    setupHappyPath([
      { measureName: "ASHP", approvedByEmail: "approver@test.com" },
      { measureName: "Solar PV", approvedByEmail: "approver@test.com" },
    ]);

    const { req, params } = makeRequest({
      changes: [
        { hubspotDealId: "deal-3", measureName: "Solar PV", approved: true },
      ],
    });

    const res = await POST(req, { params });
    expect(res.status).toBe(200);

    await vi.waitFor(() =>
      expect(syncMeasuresFieldToHubSpotMock).toHaveBeenCalled(),
    );

    expect(syncMeasuresFieldToHubSpotMock).toHaveBeenCalledWith({
      hubspotDealId: "deal-3",
      propName: "approved_measures",
      measureNames: ["ASHP", "Solar PV"],
    });
  });

  it("also calls the audit-log sync (existing behaviour preserved)", async () => {
    setupHappyPath([
      { measureName: "EWI", approvedByEmail: "approver@test.com" },
    ]);

    const { req, params } = makeRequest({
      changes: [
        { hubspotDealId: "deal-4", measureName: "EWI", approved: true },
      ],
    });

    await POST(req, { params });

    await vi.waitFor(() =>
      expect(syncMeasureApprovalsToHubSpotMock).toHaveBeenCalled(),
    );

    expect(syncMeasureApprovalsToHubSpotMock).toHaveBeenCalledWith(
      expect.objectContaining({
        hubspotDealId: "deal-4",
        approvedMeasures: [{ measureName: "EWI", approvedByEmail: "approver@test.com" }],
      }),
    );
  });

  it("does not call HubSpot syncs when session is missing", async () => {
    mockGetServerSession.mockResolvedValue(null);

    const { req, params } = makeRequest({
      changes: [
        { hubspotDealId: "deal-5", measureName: "ASHP", approved: false },
      ],
    });

    const res = await POST(req, { params });
    expect(res.status).toBe(401);
    expect(syncMeasuresFieldToHubSpotMock).not.toHaveBeenCalled();
  });

  it("does not call HubSpot syncs when user lacks approver capability", async () => {
    mockGetServerSession.mockResolvedValue({ user: { email: "writer@test.com" } });

    // getUserId
    mockDbSelect.mockImplementationOnce(() => makeSelectChain([{ id: 99n }]));
    // hasApproverCapability → empty → no capability
    mockDbSelect.mockImplementationOnce(() => makeSelectChain([]));

    const { req, params } = makeRequest({
      changes: [
        { hubspotDealId: "deal-6", measureName: "ASHP", approved: false },
      ],
    });

    const res = await POST(req, { params });
    expect(res.status).toBe(403);
    expect(syncMeasuresFieldToHubSpotMock).not.toHaveBeenCalled();
  });
});
