import { server } from "./../../../mocks/server";
import { vi } from "vitest";
import { mkdir, access, writeFile, readFile } from "fs/promises";
import { CREDENTIALS_DIR, CREDENTIALS_FILE } from "./credentials";
import { http, HttpResponse } from "msw";
import { AxiosError } from "axios";

vi.mock("./logger", () => ({
  initLogger: vi.fn(),
  logError: vi.fn(),
}));

vi.mock("fs/promises", () => ({
  readFile: vi.fn(),
  writeFile: vi.fn(),
  access: vi.fn(),
  mkdir: vi.fn(),
}));

const startMock = {
  succeed: vi.fn(),
  fail: vi.fn(),
};

vi.mock("ora", () => ({
  default: () => ({
    start: vi.fn().mockImplementation(() => startMock),
    info: vi.fn(),
  }),
}));

vi.mock("./rollbar.mjs", () => ({
  default: vi.fn(),
}));

vi.mock("open", () => ({
  default: vi.fn(),
}));

vi.mock("node:fs", () => ({
  existsSync: vi.fn(),
}));

// Mock provideConfig directly to avoid file:// URL mocking issues
vi.mock("./provideConfig", () => ({
  default: vi.fn().mockResolvedValue({
    authDomain: "test-domain.com",
    authClientId: "test-client-id",
    audienceUrl: "test-audience-url",
  }),
}));

// Import after mocks are set up
import login, { resolveFiles, getToken } from "./login";

const makeJwt = (payload: Record<string, unknown>) => {
  const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" }))
    .toString("base64url");
  const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
  return `${header}.${body}.signature`;
};

describe("login", () => {
  beforeEach(async () => {
    vi.resetModules();

    // Re-mock provideConfig after resetModules
    const provideConfigModule = await import("./provideConfig");
    vi.mocked(provideConfigModule.default).mockResolvedValue({
      authDomain: "test-domain.com",
      authClientId: "test-client-id",
      audienceUrl: "test-audience-url",
    } as any);

    vi.mocked(readFile).mockImplementation(async () =>
      Buffer.from(`{"access_token":"mocked-token"}`),
    );

    vi.mocked(access).mockImplementation(async () => {
      throw new Error();
    });

    vi.mocked(mkdir).mockImplementation(async () => "mocked");

    vi.mocked(writeFile).mockImplementation(async () => undefined);
  });

  vi.mock("./reportErrorToRollbar", () => vi.fn());
  vi.mock("./sleep", () => vi.fn());

  it("should resolve files", async () => {
    await resolveFiles();

    expect(access).toHaveBeenCalledWith(CREDENTIALS_DIR);
    expect(mkdir).toHaveBeenCalledWith(CREDENTIALS_DIR);
    expect(writeFile).toHaveBeenCalledWith(CREDENTIALS_FILE, "");
  });

  it("should get token", async () => {
    const token = await getToken();

    expect(token).toBe("mocked-token");
  });

  it("should return a non-expired JWT access token without refreshing", async () => {
    const futureExp = Math.floor(Date.now() / 1000) + 60 * 60;
    const jwt = makeJwt({ exp: futureExp });
    vi.mocked(readFile).mockResolvedValue(
      Buffer.from(
        JSON.stringify({ access_token: jwt, refresh_token: "rt-A" }),
      ),
    );

    const token = await getToken();

    expect(token).toBe(jwt);
    expect(writeFile).not.toHaveBeenCalled();
  });

  it("should refresh and persist a new access token when the cached one is expired", async () => {
    const pastExp = Math.floor(Date.now() / 1000) - 60;
    const expiredJwt = makeJwt({ exp: pastExp });
    vi.mocked(readFile).mockResolvedValue(
      Buffer.from(
        JSON.stringify({
          access_token: expiredJwt,
          refresh_token: "rt-A",
        }),
      ),
    );

    server.use(
      http.post("**/oauth/token", () =>
        HttpResponse.json({
          access_token: "fresh-access-token",
          refresh_token: "rt-B",
          expires_in: 1800,
        }),
      ),
    );

    const token = await getToken();

    expect(token).toBe("fresh-access-token");
    expect(writeFile).toHaveBeenCalledWith(
      CREDENTIALS_FILE,
      JSON.stringify({
        access_token: "fresh-access-token",
        refresh_token: "rt-B",
        expires_in: 1800,
      }),
    );
  });

  it("should return the stale access token when refresh fails", async () => {
    const pastExp = Math.floor(Date.now() / 1000) - 60;
    const expiredJwt = makeJwt({ exp: pastExp });
    vi.mocked(readFile).mockResolvedValue(
      Buffer.from(
        JSON.stringify({
          access_token: expiredJwt,
          refresh_token: "rt-A",
        }),
      ),
    );

    server.use(
      http.post("**/oauth/token", () =>
        HttpResponse.json(
          { error: "invalid_grant" },
          { status: 403 },
        ),
      ),
    );

    const token = await getToken();

    expect(token).toBe(expiredJwt);
    expect(writeFile).not.toHaveBeenCalled();
  });

  it("should return the stale access token when no refresh_token is stored", async () => {
    const pastExp = Math.floor(Date.now() / 1000) - 60;
    const expiredJwt = makeJwt({ exp: pastExp });
    vi.mocked(readFile).mockResolvedValue(
      Buffer.from(JSON.stringify({ access_token: expiredJwt })),
    );

    const token = await getToken();

    expect(token).toBe(expiredJwt);
    expect(writeFile).not.toHaveBeenCalled();
  });

  it("should login by saving token in the credentials file", async () => {
    await login();
    expect(startMock.succeed).toHaveBeenCalledWith(
      "You are successfully authenticated now!",
    );

    expect(writeFile).toHaveBeenCalledWith(
      CREDENTIALS_FILE,
      JSON.stringify({ access_token: "mocked-token " }),
    );
  });

  it("should fail to login when the response returns 500 error", async () => {
    vi.spyOn(console, "log").mockImplementation(() => undefined);
    vi.spyOn(process, "exit").mockImplementation(() => undefined as never);

    server.use(
      http.post("**/oauth/device/code", () => {
        return new HttpResponse(null, { status: 500 });
      }),
    );

    await login();

    expect(startMock.fail).toHaveBeenCalledWith(
      "Authentication failed. Please try again.",
    );

    expect((console.log as any).mock.calls[0][0]).toMatchInlineSnapshot(
      `[AxiosError: Request failed with status code 500]`,
    );
  });
});
