import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
  clearApnsRegistration,
  clearApnsRegistrationIfCurrent,
  loadApnsRegistration,
  registerApnsRegistration,
  registerApnsToken,
} from "./push-apns.js";

const tempDirs: string[] = [];

async function makeTempDir(): Promise<string> {
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-push-apns-store-test-"));
  tempDirs.push(dir);
  return dir;
}

afterEach(async () => {
  while (tempDirs.length > 0) {
    const dir = tempDirs.pop();
    if (dir) {
      await fs.rm(dir, { recursive: true, force: true });
    }
  }
});

describe("push APNs registration store", () => {
  it("stores and reloads direct APNs registrations", async () => {
    const baseDir = await makeTempDir();
    const saved = await registerApnsToken({
      nodeId: "ios-node-1",
      token: "ABCD1234ABCD1234ABCD1234ABCD1234",
      topic: "ai.openclaw.ios",
      environment: "sandbox",
      baseDir,
    });

    const loaded = await loadApnsRegistration("ios-node-1", baseDir);
    expect(loaded).toMatchObject({
      nodeId: "ios-node-1",
      transport: "direct",
      topic: "ai.openclaw.ios",
      environment: "sandbox",
      updatedAtMs: saved.updatedAtMs,
    });
    expect(loaded && loaded.transport === "direct" ? loaded.token : null).toBe(
      "abcd1234abcd1234abcd1234abcd1234",
    );
  });

  it("stores relay-backed registrations without a raw token", async () => {
    const baseDir = await makeTempDir();
    const saved = await registerApnsRegistration({
      nodeId: "ios-node-relay",
      transport: "relay",
      relayHandle: "relay-handle-123",
      sendGrant: "send-grant-123",
      installationId: "install-123",
      topic: "ai.openclaw.ios",
      environment: "production",
      distribution: "official",
      tokenDebugSuffix: " abcd-1234 ",
      baseDir,
    });

    const loaded = await loadApnsRegistration("ios-node-relay", baseDir);
    expect(saved.transport).toBe("relay");
    expect(loaded).toMatchObject({
      nodeId: "ios-node-relay",
      transport: "relay",
      relayHandle: "relay-handle-123",
      sendGrant: "send-grant-123",
      installationId: "install-123",
      topic: "ai.openclaw.ios",
      environment: "production",
      distribution: "official",
      tokenDebugSuffix: "abcd1234",
    });
    expect(loaded && "token" in loaded).toBe(false);
  });

  it("normalizes legacy direct records from disk and ignores invalid entries", async () => {
    const baseDir = await makeTempDir();
    const statePath = path.join(baseDir, "push", "apns-registrations.json");
    await fs.mkdir(path.dirname(statePath), { recursive: true });
    await fs.writeFile(
      statePath,
      `${JSON.stringify(
        {
          registrationsByNodeId: {
            " ios-node-legacy ": {
              nodeId: " ios-node-legacy ",
              token: "<ABCD1234ABCD1234ABCD1234ABCD1234>",
              topic: " ai.openclaw.ios ",
              environment: " PRODUCTION ",
              updatedAtMs: 3,
            },
            "   ": {
              nodeId: " ios-node-fallback ",
              token: "<ABCD1234ABCD1234ABCD1234ABCD1234>",
              topic: " ai.openclaw.ios ",
              updatedAtMs: 2,
            },
            "ios-node-bad-relay": {
              transport: "relay",
              nodeId: "ios-node-bad-relay",
              relayHandle: "relay-handle-123",
              sendGrant: "send-grant-123",
              installationId: "install-123",
              topic: "ai.openclaw.ios",
              environment: "production",
              distribution: "beta",
              updatedAtMs: 1,
            },
          },
        },
        null,
        2,
      )}\n`,
      "utf8",
    );

    await expect(loadApnsRegistration("ios-node-legacy", baseDir)).resolves.toMatchObject({
      nodeId: "ios-node-legacy",
      transport: "direct",
      token: "abcd1234abcd1234abcd1234abcd1234",
      topic: "ai.openclaw.ios",
      environment: "production",
      updatedAtMs: 3,
    });
    await expect(loadApnsRegistration("ios-node-fallback", baseDir)).resolves.toMatchObject({
      nodeId: "ios-node-fallback",
      transport: "direct",
      topic: "ai.openclaw.ios",
      environment: "sandbox",
      updatedAtMs: 2,
    });
    await expect(loadApnsRegistration("ios-node-bad-relay", baseDir)).resolves.toBeNull();
  });

  it("falls back cleanly for malformed or missing registration state", async () => {
    const baseDir = await makeTempDir();
    const statePath = path.join(baseDir, "push", "apns-registrations.json");
    await fs.mkdir(path.dirname(statePath), { recursive: true });
    await fs.writeFile(statePath, "[]", "utf8");

    await expect(loadApnsRegistration("ios-node-missing", baseDir)).resolves.toBeNull();
    await expect(loadApnsRegistration("   ", baseDir)).resolves.toBeNull();
    await expect(clearApnsRegistration("   ", baseDir)).resolves.toBe(false);
    await expect(clearApnsRegistration("ios-node-missing", baseDir)).resolves.toBe(false);
  });

  it("rejects invalid direct and relay registration inputs", async () => {
    const baseDir = await makeTempDir();
    const oversized = "x".repeat(257);

    await expect(
      registerApnsToken({
        nodeId: "ios-node-1",
        token: "not-a-token",
        topic: "ai.openclaw.ios",
        baseDir,
      }),
    ).rejects.toThrow("invalid APNs token");
    await expect(
      registerApnsToken({
        nodeId: "n".repeat(257),
        token: "ABCD1234ABCD1234ABCD1234ABCD1234",
        topic: "ai.openclaw.ios",
        baseDir,
      }),
    ).rejects.toThrow("nodeId required");
    await expect(
      registerApnsToken({
        nodeId: "ios-node-1",
        token: "A".repeat(513),
        topic: "ai.openclaw.ios",
        baseDir,
      }),
    ).rejects.toThrow("invalid APNs token");
    await expect(
      registerApnsToken({
        nodeId: "ios-node-1",
        token: "ABCD1234ABCD1234ABCD1234ABCD1234",
        topic: "a".repeat(256),
        baseDir,
      }),
    ).rejects.toThrow("topic required");
    await expect(
      registerApnsRegistration({
        nodeId: "ios-node-relay",
        transport: "relay",
        relayHandle: "relay-handle-123",
        sendGrant: "send-grant-123",
        installationId: "install-123",
        topic: "ai.openclaw.ios",
        environment: "staging",
        distribution: "official",
        baseDir,
      }),
    ).rejects.toThrow("relay registrations must use production environment");
    await expect(
      registerApnsRegistration({
        nodeId: "ios-node-relay",
        transport: "relay",
        relayHandle: "relay-handle-123",
        sendGrant: "send-grant-123",
        installationId: "install-123",
        topic: "ai.openclaw.ios",
        environment: "production",
        distribution: "beta",
        baseDir,
      }),
    ).rejects.toThrow("relay registrations must use official distribution");
    await expect(
      registerApnsRegistration({
        nodeId: "ios-node-relay",
        transport: "relay",
        relayHandle: oversized,
        sendGrant: "send-grant-123",
        installationId: "install-123",
        topic: "ai.openclaw.ios",
        environment: "production",
        distribution: "official",
        baseDir,
      }),
    ).rejects.toThrow("relayHandle too long");
    await expect(
      registerApnsRegistration({
        nodeId: "ios-node-relay",
        transport: "relay",
        relayHandle: "relay-handle-123",
        sendGrant: "send-grant-123",
        installationId: oversized,
        topic: "ai.openclaw.ios",
        environment: "production",
        distribution: "official",
        baseDir,
      }),
    ).rejects.toThrow("installationId too long");
    await expect(
      registerApnsRegistration({
        nodeId: "ios-node-relay",
        transport: "relay",
        relayHandle: "relay-handle-123",
        sendGrant: "x".repeat(1025),
        installationId: "install-123",
        topic: "ai.openclaw.ios",
        environment: "production",
        distribution: "official",
        baseDir,
      }),
    ).rejects.toThrow("sendGrant too long");
  });

  it("persists with a trailing newline and clears registrations", async () => {
    const baseDir = await makeTempDir();
    await registerApnsToken({
      nodeId: "ios-node-1",
      token: "ABCD1234ABCD1234ABCD1234ABCD1234",
      topic: "ai.openclaw.ios",
      baseDir,
    });

    const statePath = path.join(baseDir, "push", "apns-registrations.json");
    await expect(fs.readFile(statePath, "utf8")).resolves.toMatch(/\n$/);
    await expect(clearApnsRegistration("ios-node-1", baseDir)).resolves.toBe(true);
    await expect(loadApnsRegistration("ios-node-1", baseDir)).resolves.toBeNull();
  });

  it("only clears a registration when the stored entry still matches", async () => {
    vi.useFakeTimers();
    try {
      const baseDir = await makeTempDir();
      vi.setSystemTime(new Date("2026-03-11T00:00:00Z"));
      const stale = await registerApnsToken({
        nodeId: "ios-node-1",
        token: "ABCD1234ABCD1234ABCD1234ABCD1234",
        topic: "ai.openclaw.ios",
        environment: "sandbox",
        baseDir,
      });

      vi.setSystemTime(new Date("2026-03-11T00:00:01Z"));
      const fresh = await registerApnsToken({
        nodeId: "ios-node-1",
        token: "ABCD1234ABCD1234ABCD1234ABCD1234",
        topic: "ai.openclaw.ios",
        environment: "sandbox",
        baseDir,
      });

      await expect(
        clearApnsRegistrationIfCurrent({
          nodeId: "ios-node-1",
          registration: stale,
          baseDir,
        }),
      ).resolves.toBe(false);
      await expect(loadApnsRegistration("ios-node-1", baseDir)).resolves.toEqual(fresh);
    } finally {
      vi.useRealTimers();
    }
  });
});
