import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Mock token-storage
vi.mock("../token-storage.js", () => ({
    loadTokens: vi.fn(),
    saveTokens: vi.fn(),
    clearTokens: vi.fn(),
}));
vi.mock("../gateway-auth.js", () => ({
    refreshTokenViaGateway: vi.fn(),
}));
vi.mock("../../utils/logger.js", () => ({
    logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
}));
import { loadTokens, saveTokens, clearTokens } from "../token-storage.js";
import { isTokenExpired, refreshTokenIfNeeded, getValidAccessToken, setRefreshFn, startTokenMonitor, stopTokenMonitor, } from "../token-refresh.js";
const mockLoadTokens = vi.mocked(loadTokens);
const mockSaveTokens = vi.mocked(saveTokens);
const mockClearTokens = vi.mocked(clearTokens);
const mockRefreshFn = vi.fn();
function makeTokens(overrides = {}) {
    return {
        machine_key: "mk-test-123",
        access_token: "at-old",
        refresh_token: "rt-old",
        expires_at: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
        user_id: "user-123",
        ...overrides,
    };
}
describe("token-refresh", () => {
    beforeEach(() => {
        vi.clearAllMocks();
        setRefreshFn(mockRefreshFn);
    });
    afterEach(() => {
        stopTokenMonitor();
    });
    describe("isTokenExpired", () => {
        it("should detect expired token", () => {
            const pastTime = Math.floor(Date.now() / 1000) - 100;
            expect(isTokenExpired(pastTime)).toBe(true);
        });
        it("should not flag valid token as expired", () => {
            const futureTime = Math.floor(Date.now() / 1000) + 3600;
            expect(isTokenExpired(futureTime)).toBe(false);
        });
        it("should refresh 5 minutes before expiry", () => {
            // Token expires in 4 minutes (240 seconds) - should be considered expired (buffer is 300s)
            const almostExpired = Math.floor(Date.now() / 1000) + 240;
            expect(isTokenExpired(almostExpired)).toBe(true);
            // Token expires in 6 minutes (360 seconds) - should NOT be considered expired
            const stillValid = Math.floor(Date.now() / 1000) + 360;
            expect(isTokenExpired(stillValid)).toBe(false);
        });
    });
    describe("refreshTokenIfNeeded", () => {
        it("should not refresh valid token", async () => {
            const tokens = makeTokens();
            mockLoadTokens.mockResolvedValue(tokens);
            const result = await refreshTokenIfNeeded();
            expect(result).toBe(false);
            expect(mockRefreshFn).not.toHaveBeenCalled();
            expect(mockSaveTokens).not.toHaveBeenCalled();
        });
        it("should refresh token when expired", async () => {
            const tokens = makeTokens({
                expires_at: Math.floor(Date.now() / 1000) - 100,
            });
            mockLoadTokens.mockResolvedValue(tokens);
            const newExpiry = Math.floor(Date.now() / 1000) + 3600;
            mockRefreshFn.mockResolvedValue({
                access_token: "at-new",
                refresh_token: "rt-new",
                expires_at: newExpiry,
            });
            const result = await refreshTokenIfNeeded();
            expect(result).toBe(true);
            expect(mockRefreshFn).toHaveBeenCalledWith("rt-old");
            expect(mockSaveTokens).toHaveBeenCalledWith({
                machine_key: "mk-test-123",
                access_token: "at-new",
                refresh_token: "rt-new",
                expires_at: newExpiry,
                user_id: "user-123",
            });
        });
        it("should refresh 5 minutes before expiry", async () => {
            const tokens = makeTokens({
                expires_at: Math.floor(Date.now() / 1000) + 200, // 200s from now, within 300s buffer
            });
            mockLoadTokens.mockResolvedValue(tokens);
            const newExpiry = Math.floor(Date.now() / 1000) + 3600;
            mockRefreshFn.mockResolvedValue({
                access_token: "at-refreshed",
                refresh_token: "rt-refreshed",
                expires_at: newExpiry,
            });
            const result = await refreshTokenIfNeeded();
            expect(result).toBe(true);
            expect(mockRefreshFn).toHaveBeenCalled();
        });
        it("should throw when no tokens stored", async () => {
            mockLoadTokens.mockResolvedValue(null);
            await expect(refreshTokenIfNeeded()).rejects.toThrow("No auth tokens found");
        });
        it("should clear tokens on refresh failure", async () => {
            const tokens = makeTokens({
                expires_at: Math.floor(Date.now() / 1000) - 100,
            });
            mockLoadTokens.mockResolvedValue(tokens);
            mockRefreshFn.mockRejectedValue(new Error("Refresh failed"));
            await expect(refreshTokenIfNeeded()).rejects.toThrow("Failed to refresh token. Tokens cleared.");
            expect(mockClearTokens).toHaveBeenCalled();
        });
    });
    describe("getValidAccessToken", () => {
        it("should return valid token without refreshing", async () => {
            const tokens = makeTokens({ access_token: "at-current" });
            mockLoadTokens.mockResolvedValue(tokens);
            const result = await getValidAccessToken();
            expect(result).toBe("at-current");
            expect(mockRefreshFn).not.toHaveBeenCalled();
        });
        it("should refresh and return new token when expired", async () => {
            const expiredTokens = makeTokens({
                access_token: "at-old",
                expires_at: Math.floor(Date.now() / 1000) - 100,
            });
            const newExpiry = Math.floor(Date.now() / 1000) + 3600;
            const refreshedTokens = makeTokens({
                access_token: "at-refreshed",
                refresh_token: "rt-refreshed",
                expires_at: newExpiry,
            });
            // First call (from refreshTokenIfNeeded): returns expired tokens
            // Second call (from getValidAccessToken after refresh): returns refreshed tokens
            mockLoadTokens.mockResolvedValueOnce(expiredTokens).mockResolvedValueOnce(refreshedTokens);
            mockRefreshFn.mockResolvedValue({
                access_token: "at-refreshed",
                refresh_token: "rt-refreshed",
                expires_at: newExpiry,
            });
            const result = await getValidAccessToken();
            expect(result).toBe("at-refreshed");
        });
        it("should throw when no tokens available", async () => {
            mockLoadTokens.mockResolvedValue(null);
            await expect(getValidAccessToken()).rejects.toThrow("No auth tokens found");
        });
    });
    describe("startTokenMonitor / stopTokenMonitor", () => {
        beforeEach(() => {
            vi.useFakeTimers();
        });
        afterEach(() => {
            stopTokenMonitor();
            vi.useRealTimers();
        });
        it("should periodically check token expiry", async () => {
            const tokens = makeTokens();
            mockLoadTokens.mockResolvedValue(tokens);
            startTokenMonitor();
            // Advance by 60 seconds (one interval tick)
            await vi.advanceTimersByTimeAsync(60_000);
            expect(mockLoadTokens).toHaveBeenCalled();
        });
        it("should refresh expired token on monitor tick", async () => {
            const tokens = makeTokens({
                expires_at: Math.floor(Date.now() / 1000) - 100,
            });
            mockLoadTokens.mockResolvedValue(tokens);
            const newExpiry = Math.floor(Date.now() / 1000) + 3600;
            mockRefreshFn.mockResolvedValue({
                access_token: "at-monitor",
                refresh_token: "rt-monitor",
                expires_at: newExpiry,
            });
            startTokenMonitor();
            await vi.advanceTimersByTimeAsync(60_000);
            expect(mockRefreshFn).toHaveBeenCalledWith("rt-old");
            expect(mockSaveTokens).toHaveBeenCalled();
        });
        it("should not throw on monitor errors", async () => {
            mockLoadTokens.mockRejectedValue(new Error("disk error"));
            startTokenMonitor();
            // Should not throw — errors are caught and logged
            await vi.advanceTimersByTimeAsync(60_000);
        });
        it("should stop monitoring when stopTokenMonitor is called", async () => {
            const tokens = makeTokens();
            mockLoadTokens.mockResolvedValue(tokens);
            startTokenMonitor();
            stopTokenMonitor();
            await vi.advanceTimersByTimeAsync(120_000);
            // loadTokens should not have been called since the monitor was stopped before any tick
            expect(mockLoadTokens).not.toHaveBeenCalled();
        });
        it("should be idempotent when called multiple times", () => {
            startTokenMonitor();
            startTokenMonitor(); // Should not create a second interval
            stopTokenMonitor();
        });
    });
});
//# sourceMappingURL=token-refresh.test.js.map