/**
 * Tests for the Pro-Trial-Reward trigger inside `tryAwardProTrial`.
 *
 * Critical path (Ahmed-priority): the 8-line vitest that would have prevented
 * a 500-cascade. Covers:
 *   - happy path: free + all 6 fields filled + never used → trial awarded
 *   - free + one field missing → no trial
 *   - already pro → no trial
 *   - trial already used → no re-trial
 *
 * + zod-validation on the patch endpoint (anonymity / range / enum sanity).
 */
import { describe, expect, it, vi, beforeEach } from "vitest";

// vi.hoisted ensures the mock-state object exists when vi.mock-factory runs
// (vi.mock is hoisted to top-of-file by vitest's transformer).
const mocks = vi.hoisted(() => ({
  profile: {
    findUnique: vi.fn(),
    update: vi.fn(),
  },
}));

vi.mock("../../server/utils/prisma", () => ({
  usePrisma: () => ({
    profile: mocks.profile,
    $transaction: async (cb: (tx: { profile: typeof mocks.profile }) => Promise<unknown>) =>
      cb({ profile: mocks.profile }),
  }),
}));

import {
  tryAwardProTrial,
  updateDemographics,
  withdrawDemographics,
} from "../../server/db/profile";

const mockProfile = mocks.profile;

const FULL_DEMOGRAPHICS = {
  birthYear: 1989,
  gender: "male",
  maritalStatus: "single",
  employmentStatus: "employed",
  bundesland: "DE-BY",
  city: "München",
};

beforeEach(() => {
  vi.clearAllMocks();
});

describe("tryAwardProTrial — happy path", () => {
  it("awards 7-day trial when free + all fields filled + never used", async () => {
    mockProfile.findUnique.mockResolvedValueOnce({
      plan: "free",
      proTrialUsedAt: null,
      ...FULL_DEMOGRAPHICS,
    });
    mockProfile.update.mockResolvedValueOnce({});

    const before = Date.now();
    const result = await tryAwardProTrial("user-1");

    expect(result.trialAwarded).toBe(true);
    expect(result.expiresAt).toBeInstanceOf(Date);
    const expiresMs = result.expiresAt!.getTime();
    const expected = before + 7 * 24 * 60 * 60 * 1000;
    expect(expiresMs).toBeGreaterThanOrEqual(expected - 1000);
    expect(expiresMs).toBeLessThanOrEqual(expected + 5_000);

    expect(mockProfile.update).toHaveBeenCalledWith({
      where: { id: "user-1" },
      data: expect.objectContaining({
        plan: "pro",
        proTrialSource: "demographics_complete",
      }),
    });
  });
});

describe("tryAwardProTrial — guard rails", () => {
  it("does NOT award trial when one demographic field is missing", async () => {
    mockProfile.findUnique.mockResolvedValueOnce({
      plan: "free",
      proTrialUsedAt: null,
      ...FULL_DEMOGRAPHICS,
      city: null, // missing
    });

    const result = await tryAwardProTrial("user-1");

    expect(result.trialAwarded).toBe(false);
    expect(result.expiresAt).toBeNull();
    expect(mockProfile.update).not.toHaveBeenCalled();
  });

  it("does NOT award trial when user is already pro", async () => {
    mockProfile.findUnique.mockResolvedValueOnce({
      plan: "pro",
      proTrialUsedAt: null,
      ...FULL_DEMOGRAPHICS,
    });

    const result = await tryAwardProTrial("user-1");

    expect(result.trialAwarded).toBe(false);
    expect(mockProfile.update).not.toHaveBeenCalled();
  });

  it("does NOT award trial when user is legend", async () => {
    mockProfile.findUnique.mockResolvedValueOnce({
      plan: "legend",
      proTrialUsedAt: null,
      ...FULL_DEMOGRAPHICS,
    });

    const result = await tryAwardProTrial("user-1");

    expect(result.trialAwarded).toBe(false);
    expect(mockProfile.update).not.toHaveBeenCalled();
  });

  it("does NOT award trial when proTrialUsedAt already set (no re-trial)", async () => {
    mockProfile.findUnique.mockResolvedValueOnce({
      plan: "free",
      proTrialUsedAt: new Date("2026-04-01"),
      ...FULL_DEMOGRAPHICS,
    });

    const result = await tryAwardProTrial("user-1");

    expect(result.trialAwarded).toBe(false);
    expect(mockProfile.update).not.toHaveBeenCalled();
  });

  it("does NOT award when birthYear is 0/null (treats as missing)", async () => {
    mockProfile.findUnique.mockResolvedValueOnce({
      plan: "free",
      proTrialUsedAt: null,
      ...FULL_DEMOGRAPHICS,
      birthYear: null,
    });

    const result = await tryAwardProTrial("user-1");
    expect(result.trialAwarded).toBe(false);
  });
});

describe("updateDemographics — first-touch consent stamp", () => {
  it("sets demographicsConsentAt = NOW on first non-null write", async () => {
    mockProfile.findUnique.mockResolvedValueOnce({
      demographicsConsentAt: null,
      demographicsWithdrawnAt: null,
    });
    mockProfile.update.mockResolvedValueOnce({});

    await updateDemographics("user-1", { birthYear: 1989 });

    expect(mockProfile.update).toHaveBeenCalledWith({
      where: { id: "user-1" },
      data: expect.objectContaining({
        birthYear: 1989,
        demographicsConsentAt: expect.any(Date),
      }),
    });
  });

  it("does NOT re-stamp consentAt when already set", async () => {
    mockProfile.findUnique.mockResolvedValueOnce({
      demographicsConsentAt: new Date("2026-01-01"),
      demographicsWithdrawnAt: null,
    });
    mockProfile.update.mockResolvedValueOnce({});

    await updateDemographics("user-1", { profession: "Pflege" });

    const call = mockProfile.update.mock.calls[0]?.[0] as {
      data: Record<string, unknown>;
    };
    expect(call.data).not.toHaveProperty("demographicsConsentAt");
  });

  it("clears demographicsWithdrawnAt when re-granted (re-fill after withdrawal)", async () => {
    mockProfile.findUnique.mockResolvedValueOnce({
      demographicsConsentAt: new Date("2026-01-01"),
      demographicsWithdrawnAt: new Date("2026-03-01"),
    });
    mockProfile.update.mockResolvedValueOnce({});

    await updateDemographics("user-1", { birthYear: 1989 });

    expect(mockProfile.update).toHaveBeenCalledWith({
      where: { id: "user-1" },
      data: expect.objectContaining({
        demographicsWithdrawnAt: null,
      }),
    });
  });

  it("does NOT stamp consent when patch contains only nulls (clearing)", async () => {
    mockProfile.findUnique.mockResolvedValueOnce({
      demographicsConsentAt: null,
      demographicsWithdrawnAt: null,
    });
    mockProfile.update.mockResolvedValueOnce({});

    await updateDemographics("user-1", { city: null });

    const call = mockProfile.update.mock.calls[0]?.[0] as {
      data: Record<string, unknown>;
    };
    expect(call.data).not.toHaveProperty("demographicsConsentAt");
  });
});

describe("withdrawDemographics", () => {
  it("nulls all demographic fields + sets withdrawnAt + keeps consentAt", async () => {
    mockProfile.update.mockResolvedValueOnce({});

    await withdrawDemographics("user-1");

    expect(mockProfile.update).toHaveBeenCalledWith({
      where: { id: "user-1" },
      data: expect.objectContaining({
        birthYear: null,
        gender: null,
        maritalStatus: null,
        profession: null,       // legacy field also cleared
        employmentStatus: null,
        shiftWork: null,
        industry: null,
        jobTenure: null,
        bundesland: null,
        city: null,
        demographicsWithdrawnAt: expect.any(Date),
      }),
    });
    const call = mockProfile.update.mock.calls[0]?.[0] as {
      data: Record<string, unknown>;
    };
    // consent stamp must NOT be wiped (audit trail)
    expect(call.data).not.toHaveProperty("demographicsConsentAt");
  });
});
