/**
 * Tests for voice quota DB layer (server/db/voiceQuota.ts).
 *
 * Covers:
 *   - Free: partial consumption → correct remaining
 *   - Free: exhausted quota → 0 remaining
 *   - Day-rollover: stale resetAt → auto-reset to plan default
 *   - Legend: unlimited → consumeVoiceQuota is a no-op
 *   - estimateAudioSeconds: basic sanity
 */
import { describe, expect, it, vi, beforeEach } from "vitest";

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

const mocks = vi.hoisted(() => ({
  profile: {
    findUnique: vi.fn(),
    update: vi.fn(),
  },
}));

vi.mock("../../server/utils/prisma", () => ({
  usePrisma: () => ({
    profile: mocks.profile,
  }),
}));

import {
  getRemainingVoiceQuota,
  consumeVoiceQuota,
  estimateAudioSeconds,
} from "../../server/db/voiceQuota";

beforeEach(() => {
  vi.clearAllMocks();
  mocks.profile.update.mockResolvedValue({});
});

// ─── estimateAudioSeconds ─────────────────────────────────────────────────────

describe("estimateAudioSeconds", () => {
  it("returns at least 1 for empty/very short text", () => {
    expect(estimateAudioSeconds("")).toBe(1);
    expect(estimateAudioSeconds("Hi")).toBe(1);
  });

  it("estimates ~13 chars/sec", () => {
    // 130 chars → ~10 sec
    const text = "a".repeat(130);
    expect(estimateAudioSeconds(text)).toBe(10);
  });

  it("rounds up", () => {
    // 14 chars → ceil(14/13) = 2
    const text = "a".repeat(14);
    expect(estimateAudioSeconds(text)).toBe(2);
  });
});

// ─── getRemainingVoiceQuota ───────────────────────────────────────────────────

describe("getRemainingVoiceQuota — free plan (60s)", () => {
  it("returns 30s remaining after 30s consumed (same day)", async () => {
    const todayMidnight = new Date();
    todayMidnight.setUTCHours(0, 0, 0, 0);

    mocks.profile.findUnique.mockResolvedValueOnce({
      voiceSecondsUsedToday: 30,
      voiceQuotaResetAt: todayMidnight, // reset is today → no rollover
    });

    const remaining = await getRemainingVoiceQuota("user-1", "free");
    expect(remaining).toBe(30); // 60 - 30
    expect(mocks.profile.update).not.toHaveBeenCalled();
  });

  it("returns 0 when full 60s consumed", async () => {
    const todayMidnight = new Date();
    todayMidnight.setUTCHours(0, 0, 0, 0);

    mocks.profile.findUnique.mockResolvedValueOnce({
      voiceSecondsUsedToday: 60,
      voiceQuotaResetAt: todayMidnight,
    });

    const remaining = await getRemainingVoiceQuota("user-1", "free");
    expect(remaining).toBe(0);
  });

  it("clamps to 0 when over-consumed (no negative)", async () => {
    const todayMidnight = new Date();
    todayMidnight.setUTCHours(0, 0, 0, 0);

    mocks.profile.findUnique.mockResolvedValueOnce({
      voiceSecondsUsedToday: 999,
      voiceQuotaResetAt: todayMidnight,
    });

    const remaining = await getRemainingVoiceQuota("user-1", "free");
    expect(remaining).toBe(0);
  });
});

describe("getRemainingVoiceQuota — day rollover", () => {
  it("resets to full quota when resetAt is yesterday", async () => {
    const yesterday = new Date();
    yesterday.setUTCDate(yesterday.getUTCDate() - 1);
    yesterday.setUTCHours(0, 0, 0, 0);

    mocks.profile.findUnique.mockResolvedValueOnce({
      voiceSecondsUsedToday: 60, // was fully consumed yesterday
      voiceQuotaResetAt: yesterday,
    });

    const remaining = await getRemainingVoiceQuota("user-1", "free");
    expect(remaining).toBe(60); // full plan quota after reset

    // Should have reset the counter
    expect(mocks.profile.update).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { id: "user-1" },
        data: expect.objectContaining({
          voiceSecondsUsedToday: 0,
        }),
      }),
    );
  });

  it("resets when voiceQuotaResetAt is null (first ever use)", async () => {
    mocks.profile.findUnique.mockResolvedValueOnce({
      voiceSecondsUsedToday: 0,
      voiceQuotaResetAt: null,
    });

    const remaining = await getRemainingVoiceQuota("user-1", "pro");
    expect(remaining).toBe(300); // full pro quota
    expect(mocks.profile.update).toHaveBeenCalled();
  });
});

// ─── getRemainingVoiceQuota — Legend (unlimited) ──────────────────────────────

describe("getRemainingVoiceQuota — legend plan (unlimited)", () => {
  it("returns Infinity without touching DB", async () => {
    const remaining = await getRemainingVoiceQuota("user-legend", "legend");
    expect(remaining).toBe(Infinity);
    expect(mocks.profile.findUnique).not.toHaveBeenCalled();
    expect(mocks.profile.update).not.toHaveBeenCalled();
  });
});

// ─── consumeVoiceQuota ────────────────────────────────────────────────────────

describe("consumeVoiceQuota", () => {
  it("increments counter for free plan", async () => {
    await consumeVoiceQuota("user-1", "free", 30);

    expect(mocks.profile.update).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { id: "user-1" },
        data: expect.objectContaining({
          voiceSecondsUsedToday: { increment: 30 },
        }),
      }),
    );
  });

  it("increments counter for pro plan", async () => {
    await consumeVoiceQuota("user-1", "pro", 45);

    expect(mocks.profile.update).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          voiceSecondsUsedToday: { increment: 45 },
        }),
      }),
    );
  });

  it("is a no-op for legend plan (unlimited)", async () => {
    await consumeVoiceQuota("user-legend", "legend", 120);
    expect(mocks.profile.update).not.toHaveBeenCalled();
  });

  it("rounds fractional seconds to whole number", async () => {
    await consumeVoiceQuota("user-1", "free", 7.7);

    expect(mocks.profile.update).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          voiceSecondsUsedToday: { increment: 8 }, // Math.round(7.7)
        }),
      }),
    );
  });

  it("clamps negative seconds to 0", async () => {
    await consumeVoiceQuota("user-1", "free", -5);

    expect(mocks.profile.update).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          voiceSecondsUsedToday: { increment: 0 },
        }),
      }),
    );
  });
});
