import { ApiService } from "../abstractions/api.service";
import { HibpApiService } from "../dirt/services/hibp-api.service";
import { CryptoFunctionService } from "../key-management/crypto/abstractions/crypto-function.service";

import { AuditService } from "./audit.service";

jest.useFakeTimers();

// Polyfill global Request for Jest environment if not present
if (typeof global.Request === "undefined") {
  global.Request = jest.fn((input: string | URL, init?: RequestInit) => {
    return { url: typeof input === "string" ? input : input.toString(), ...init };
  }) as any;
}

describe("AuditService", () => {
  let auditService: AuditService;
  let mockCrypto: jest.Mocked<CryptoFunctionService>;
  let mockApi: jest.Mocked<ApiService>;
  let mockHibpApi: jest.Mocked<HibpApiService>;

  beforeEach(() => {
    mockCrypto = {
      hash: jest.fn().mockResolvedValue(new Uint8Array([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff])),
    } as unknown as jest.Mocked<CryptoFunctionService>;

    mockApi = {
      nativeFetch: jest.fn().mockResolvedValue({
        text: jest.fn().mockResolvedValue(`CDDEEFF:4\nDDEEFF:2\n123456:1`),
      }),
    } as unknown as jest.Mocked<ApiService>;

    mockHibpApi = {
      getHibpBreach: jest.fn(),
    } as unknown as jest.Mocked<HibpApiService>;

    auditService = new AuditService(mockCrypto, mockApi, mockHibpApi, 2);
  });

  it("should not exceed max concurrent passwordLeaked requests", async () => {
    const inFlight: string[] = [];
    const maxInFlight: number[] = [];

    // Patch fetchLeakedPasswordCount to track concurrency
    const origFetch = (auditService as any).fetchLeakedPasswordCount.bind(auditService);
    jest
      .spyOn(auditService as any, "fetchLeakedPasswordCount")
      .mockImplementation(async (password: string) => {
        inFlight.push(password);
        maxInFlight.push(inFlight.length);
        // Simulate async work to allow concurrency limiter to take effect
        await new Promise((resolve) => setTimeout(resolve, 100));
        inFlight.splice(inFlight.indexOf(password), 1);
        return origFetch(password);
      });

    const p1 = auditService.passwordLeaked("password1");
    const p2 = auditService.passwordLeaked("password2");
    const p3 = auditService.passwordLeaked("password3");
    const p4 = auditService.passwordLeaked("password4");

    jest.advanceTimersByTime(250);

    // Flush all pending timers and microtasks
    await jest.runAllTimersAsync();
    await Promise.all([p1, p2, p3, p4]);

    // The max value in maxInFlight should not exceed 2 (the concurrency limit)
    expect(Math.max(...maxInFlight)).toBeLessThanOrEqual(2);
    expect((auditService as any).fetchLeakedPasswordCount).toHaveBeenCalledTimes(4);
    expect(mockCrypto.hash).toHaveBeenCalledTimes(4);
    expect(mockApi.nativeFetch).toHaveBeenCalledTimes(4);
  });

  it("should include Add-Padding header when checking leaked passwords", async () => {
    const result = await auditService.passwordLeaked("password");

    expect(result).toBe(4);
    expect(mockApi.nativeFetch).toHaveBeenCalledTimes(1);
    const request = mockApi.nativeFetch.mock.calls[0][0] as any;
    expect(request.url).toBe("https://api.pwnedpasswords.com/range/AABBC");
    expect(request.headers).toEqual(expect.objectContaining({ "Add-Padding": "true" }));
  });

  it("should return empty array for breachedAccounts when no breaches found", async () => {
    // Server returns 200 with empty array (correct REST semantics)
    mockHibpApi.getHibpBreach.mockResolvedValueOnce([]);
    const result = await auditService.breachedAccounts("user@example.com");
    expect(result).toEqual([]);
  });

  it("should propagate errors from breachedAccounts", async () => {
    const error = new Error("API error");
    mockHibpApi.getHibpBreach.mockRejectedValueOnce(error);
    await expect(auditService.breachedAccounts("user@example.com")).rejects.toBe(error);
  });
});
