import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { SessionRefreshTimerImpl } from "./session-refresh-timer";

const createJwt = (exp: number): string => {
  const payload = btoa(JSON.stringify({ exp }))
    .replace(/=/g, "")
    .replace(/\+/g, "-")
    .replace(/\//g, "_");
  return `header.${payload}.sig`;
};

const createMockTokenManager = (overrides: Record<string, any> = {}) => ({
  getToken: vi.fn().mockReturnValue(createJwt(Math.floor(Date.now() / 1000) + 3600)),
  setInitialTokens: vi.fn(),
  refreshToken: vi.fn().mockResolvedValue("new_token"),
  ...overrides,
});

const createMockActivityMonitor = (lastActivity = Date.now()) => ({
  start: vi.fn(),
  stop: vi.fn(),
  getLastActivityTimestamp: vi.fn().mockReturnValue(lastActivity),
});

describe("SessionRefreshTimer", () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
    vi.clearAllMocks();
  });

  it("should skip tick when there is no recent activity", async () => {
    const tokenManager = createMockTokenManager({
      getToken: vi.fn().mockReturnValue(createJwt(Math.floor(Date.now() / 1000) + 60)),
    });
    const timer = new SessionRefreshTimerImpl(tokenManager, createMockActivityMonitor(0));

    timer.start();
    await vi.advanceTimersByTimeAsync(30_000);

    expect(tokenManager.refreshToken).not.toHaveBeenCalled();
  });

  it("should skip tick when token is not near expiry", async () => {
    const tokenManager = createMockTokenManager({
      getToken: vi.fn().mockReturnValue(createJwt(Math.floor(Date.now() / 1000) + 3600)),
    });
    const timer = new SessionRefreshTimerImpl(tokenManager, createMockActivityMonitor());

    timer.start();
    await vi.advanceTimersByTimeAsync(30_000);

    expect(tokenManager.refreshToken).not.toHaveBeenCalled();
  });

  it("should refresh when token is near expiry and user is active", async () => {
    const tokenManager = createMockTokenManager({
      getToken: vi.fn().mockReturnValue(createJwt(Math.floor(Date.now() / 1000) + 60)),
    });
    const timer = new SessionRefreshTimerImpl(tokenManager, createMockActivityMonitor());

    timer.start();
    await vi.advanceTimersByTimeAsync(30_000);

    expect(tokenManager.refreshToken).toHaveBeenCalledTimes(1);
  });

  it("should refresh when token is already expired and user is active", async () => {
    const tokenManager = createMockTokenManager({
      getToken: vi.fn().mockReturnValue(createJwt(Math.floor(Date.now() / 1000) - 60)),
    });
    const timer = new SessionRefreshTimerImpl(tokenManager, createMockActivityMonitor());

    timer.start();
    await vi.advanceTimersByTimeAsync(30_000);

    expect(tokenManager.refreshToken).toHaveBeenCalledTimes(1);
  });

  it("should not trigger concurrent refreshes", async () => {
    let resolveRefresh!: () => void;
    const tokenManager = createMockTokenManager({
      getToken: vi.fn().mockReturnValue(createJwt(Math.floor(Date.now() / 1000) + 60)),
      refreshToken: vi.fn().mockImplementation(
        () => new Promise((resolve) => { resolveRefresh = () => resolve("new_token"); }),
      ),
    });
    const timer = new SessionRefreshTimerImpl(tokenManager, createMockActivityMonitor());

    timer.start();
    await vi.advanceTimersByTimeAsync(30_000);
    await vi.advanceTimersByTimeAsync(30_000);

    resolveRefresh();
    await Promise.resolve();

    expect(tokenManager.refreshToken).toHaveBeenCalledTimes(1);
  });
});
