import { assert, describe, it, vi, afterEach, beforeEach } from "vitest";
import type { HomeAssistant } from "../../src/types";
import {
  addBrandsAuth,
  brandsUrl,
  clearBrandsTokenRefresh,
  fetchAndScheduleBrandsAccessToken,
  fetchBrandsAccessToken,
  hardwareBrandsUrl,
  scheduleBrandsTokenRefresh,
} from "../../src/util/brands-url";

// NOTE: the cached brands token is module-level state that persists across
// tests. The "without a token" assertions below must run before any test
// fetches a token, so this block is intentionally declared first.
describe("Brands URLs without a token", () => {
  // The brands API requires a token; until one is fetched the URL builders
  // return an empty src so no token-less request (which 401s) fires. Components
  // re-render once the token arrives and recompute the URL.
  it("brandsUrl returns an empty src", () => {
    assert.strictEqual(
      brandsUrl(
        { domain: "cloud", type: "logo" },
        "http://homeassistant.local:8123"
      ),
      ""
    );
  });

  it("hardwareBrandsUrl returns an empty src", () => {
    assert.strictEqual(
      hardwareBrandsUrl(
        { category: "boards", manufacturer: "raspberry_pi" },
        "http://homeassistant.local:8123"
      ),
      ""
    );
  });

  it("addBrandsAuth returns an empty src for brands URLs", () => {
    assert.strictEqual(
      addBrandsAuth(
        "/api/brands/integration/demo/icon.png",
        "http://homeassistant.local:8123"
      ),
      ""
    );
  });

  it("addBrandsAuth returns non-brands URLs unchanged", () => {
    assert.strictEqual(
      addBrandsAuth(
        "/api/camera_proxy/camera.foo?token=abc",
        "http://homeassistant.local:8123"
      ),
      "/api/camera_proxy/camera.foo?token=abc"
    );
  });
});

describe("Generate brands Url", () => {
  // Fetch a token before these run so the URL builders produce loadable URLs.
  beforeEach(async () => {
    const mockHass = {
      callWS: async () => ({ token: "test-token-123" }),
    } as unknown as HomeAssistant;
    await fetchBrandsAccessToken(mockHass);
  });

  it("Generate logo brands url for cloud component", () => {
    assert.strictEqual(
      brandsUrl(
        { domain: "cloud", type: "logo" },
        "http://homeassistant.local:8123"
      ),
      "http://homeassistant.local:8123/api/brands/integration/cloud/logo.png?token=test-token-123"
    );
  });
  it("Generate icon brands url for cloud component", () => {
    assert.strictEqual(
      brandsUrl(
        { domain: "cloud", type: "icon" },
        "http://homeassistant.local:8123"
      ),
      "http://homeassistant.local:8123/api/brands/integration/cloud/icon.png?token=test-token-123"
    );
  });

  it("Generate dark theme optimized logo brands url for cloud component", () => {
    assert.strictEqual(
      brandsUrl(
        { domain: "cloud", type: "logo", darkOptimized: true },
        "http://homeassistant.local:8123"
      ),
      "http://homeassistant.local:8123/api/brands/integration/cloud/dark_logo.png?token=test-token-123"
    );
  });
});

describe("addBrandsAuth", () => {
  it("Returns non-brands URLs unchanged", () => {
    assert.strictEqual(
      addBrandsAuth(
        "/api/camera_proxy/camera.foo?token=abc",
        "http://homeassistant.local:8123"
      ),
      "/api/camera_proxy/camera.foo?token=abc"
    );
  });

  it("Appends token to brands URL when token is available", async () => {
    const mockHass = {
      callWS: async () => ({ token: "test-token-123" }),
    } as unknown as HomeAssistant;
    await fetchBrandsAccessToken(mockHass);

    assert.strictEqual(
      addBrandsAuth(
        "/api/brands/integration/demo/icon.png",
        "http://homeassistant.local:8123"
      ),
      "http://homeassistant.local:8123/api/brands/integration/demo/icon.png?token=test-token-123"
    );
  });

  it("Replaces existing token param instead of duplicating", async () => {
    const mockHass = {
      callWS: async () => ({ token: "new-token" }),
    } as unknown as HomeAssistant;
    await fetchBrandsAccessToken(mockHass);

    assert.strictEqual(
      addBrandsAuth(
        "/api/brands/integration/demo/icon.png?token=old-token",
        "http://homeassistant.local:8123"
      ),
      "http://homeassistant.local:8123/api/brands/integration/demo/icon.png?token=new-token"
    );
  });
});

describe("scheduleBrandsTokenRefresh", () => {
  afterEach(() => {
    clearBrandsTokenRefresh();
    vi.restoreAllMocks();
    vi.useRealTimers();
  });

  it("Refreshes the token after 30 minutes", async () => {
    vi.useFakeTimers();
    let callCount = 0;
    const mockHass = {
      callWS: async () => {
        callCount++;
        return { token: `token-${callCount}` };
      },
    } as unknown as HomeAssistant;

    await fetchBrandsAccessToken(mockHass);
    assert.strictEqual(callCount, 1);
    assert.strictEqual(
      brandsUrl(
        { domain: "test", type: "icon" },
        "http://homeassistant.local:8123"
      ),
      "http://homeassistant.local:8123/api/brands/integration/test/icon.png?token=token-1"
    );

    scheduleBrandsTokenRefresh(mockHass);

    // Advance 30 minutes
    await vi.advanceTimersByTimeAsync(30 * 60 * 1000);
    assert.strictEqual(callCount, 2);
    assert.strictEqual(
      brandsUrl(
        { domain: "test", type: "icon" },
        "http://homeassistant.local:8123"
      ),
      "http://homeassistant.local:8123/api/brands/integration/test/icon.png?token=token-2"
    );
  });

  it("Does not refresh before 30 minutes", async () => {
    vi.useFakeTimers();
    let callCount = 0;
    const mockHass = {
      callWS: async () => {
        callCount++;
        return { token: `token-${callCount}` };
      },
    } as unknown as HomeAssistant;

    await fetchBrandsAccessToken(mockHass);
    scheduleBrandsTokenRefresh(mockHass);

    // Advance 29 minutes — should not have refreshed
    await vi.advanceTimersByTimeAsync(29 * 60 * 1000);
    assert.strictEqual(callCount, 1);
  });

  it("clearBrandsTokenRefresh stops the interval", async () => {
    vi.useFakeTimers();
    let callCount = 0;
    const mockHass = {
      callWS: async () => {
        callCount++;
        return { token: `token-${callCount}` };
      },
    } as unknown as HomeAssistant;

    await fetchBrandsAccessToken(mockHass);
    scheduleBrandsTokenRefresh(mockHass);
    clearBrandsTokenRefresh();

    // Advance 30 minutes — should not have refreshed because we cleared
    await vi.advanceTimersByTimeAsync(30 * 60 * 1000);
    assert.strictEqual(callCount, 1);
  });
});

describe("fetchAndScheduleBrandsAccessToken", () => {
  afterEach(() => {
    clearBrandsTokenRefresh();
    vi.restoreAllMocks();
    vi.useRealTimers();
  });

  it("retries with backoff until the WS call succeeds, returns true when the token changed", async () => {
    vi.useFakeTimers();
    let callCount = 0;
    const mockHass = {
      callWS: async () => {
        callCount++;
        if (callCount < 3) {
          throw new Error("unknown_command");
        }
        return { token: `retry-token-${callCount}` };
      },
    } as unknown as HomeAssistant;

    const promise = fetchAndScheduleBrandsAccessToken(mockHass);

    // First attempt fires immediately, fails
    await vi.advanceTimersByTimeAsync(0);
    assert.strictEqual(callCount, 1);

    // 500ms backoff → second attempt fails
    await vi.advanceTimersByTimeAsync(500);
    assert.strictEqual(callCount, 2);

    // 1000ms backoff → third attempt succeeds
    await vi.advanceTimersByTimeAsync(1000);

    const changed = await promise;
    assert.strictEqual(changed, true);
    assert.strictEqual(callCount, 3);
    assert.strictEqual(
      brandsUrl(
        { domain: "test", type: "icon" },
        "http://homeassistant.local:8123"
      ),
      "http://homeassistant.local:8123/api/brands/integration/test/icon.png?token=retry-token-3"
    );
  });

  it("returns false when the backend returns the same token (no UI change needed)", async () => {
    const mockHass = {
      callWS: async () => ({ token: "stable-token" }),
    } as unknown as HomeAssistant;

    // Prime the cached token
    await fetchBrandsAccessToken(mockHass);

    // Same token returned → no change
    const changed = await fetchAndScheduleBrandsAccessToken(mockHass);
    assert.strictEqual(changed, false);
  });

  it("returns false after all retries fail (e.g. older backend)", async () => {
    vi.useFakeTimers();
    let callCount = 0;
    const mockHass = {
      callWS: async () => {
        callCount++;
        throw new Error("unknown_command");
      },
    } as unknown as HomeAssistant;

    const promise = fetchAndScheduleBrandsAccessToken(mockHass);

    // Exhaust all retry delays: 500 + 1000 + 2000 + 5000 + 10000 + 15000
    await vi.advanceTimersByTimeAsync(33500);

    const changed = await promise;
    assert.strictEqual(changed, false);
    // 1 immediate attempt + 6 retries = 7 attempts
    assert.strictEqual(callCount, 7);
  });
});
