// End-to-end tests for the desktop-side affiliate / referral handshake.
//
// We stand up an in-process HTTP server that implements the same contract
// as openswarm-cloud's /api/install/{mint,bind,lookup} endpoints (in-memory
// state, no SQLite). The Electron module's polling code talks to this
// server over real fetch over real loopback TCP, which is as realistic as
// it gets without booting the actual cloud Hono app.
//
// We then drive both halves of the flow:
//   * The "user clicks Download on the landing page" half: mint() to get an
//     install_token, stash it where the test's "welcome page" simulator can
//     find it.
//   * The "user installs the app" half: maybeRunFirstLaunchHandshake() with
//     a fake shell that captures the welcome URL, then we simulate the
//     welcome page by calling /api/install/bind from the test before the
//     poll loop times out.
//
// Polling cadence is squeezed via env vars (OPENSWARM_AFFILIATE_POLL_*) so
// the suite finishes in milliseconds instead of seconds.

const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const os = require("node:os");
const http = require("node:http");
const crypto = require("node:crypto");

// Force the tracking module to use tight polling well before requiring it,
// because the constants are read at module-load time.
process.env.OPENSWARM_AFFILIATE_POLL_INTERVAL_MS = "20";
process.env.OPENSWARM_AFFILIATE_POLL_MAX_ATTEMPTS = "30";

const affiliateTracking = require("./affiliateTracking");

// --- in-memory mock cloud --------------------------------------------------

function makeMockCloud() {
  // Mirrors the install_tokens table.
  const tokens = new Map();

  const server = http.createServer((req, res) => {
    const url = new URL(req.url, `http://${req.headers.host}`);
    const send = (status, body) => {
      const json = JSON.stringify(body);
      res.writeHead(status, { "Content-Type": "application/json" });
      res.end(json);
    };
    const readBody = () =>
      new Promise((resolve) => {
        let buf = "";
        req.on("data", (c) => (buf += c));
        req.on("end", () => {
          try {
            resolve(JSON.parse(buf || "{}"));
          } catch {
            resolve(null);
          }
        });
      });

    (async () => {
      if (req.method === "POST" && url.pathname === "/api/install/mint") {
        const body = await readBody();
        if (!body || typeof body.ref !== "string" || !body.ref) {
          return send(400, { error: "ref required" });
        }
        const token = crypto.randomBytes(32).toString("base64url");
        tokens.set(token, {
          ref: body.ref,
          app_install_id: null,
          bound_at: null,
          expires_at: Date.now() + 24 * 60 * 60 * 1000,
        });
        return send(200, { install_token: token, expires_at: Date.now() + 24 * 60 * 60 * 1000 });
      }

      if (req.method === "POST" && url.pathname === "/api/install/bind") {
        const body = await readBody();
        if (!body || typeof body.install_token !== "string" || typeof body.app_install_id !== "string") {
          return send(400, { error: "install_token + app_install_id required" });
        }
        const row = tokens.get(body.install_token);
        if (!row) return send(404, { error: "not found" });
        if (row.expires_at < Date.now()) return send(410, { error: "expired" });
        if (row.app_install_id && row.app_install_id !== body.app_install_id) {
          return send(409, { error: "already bound" });
        }
        row.app_install_id = body.app_install_id;
        row.bound_at = Date.now();
        return send(200, { ok: true, ref: row.ref });
      }

      if (req.method === "GET" && url.pathname === "/api/install/lookup") {
        const appInstallId = url.searchParams.get("app_install_id") || "";
        if (!/^[A-Za-z0-9_-]{8,128}$/.test(appInstallId)) {
          return send(400, { error: "bad app_install_id" });
        }
        for (const row of tokens.values()) {
          if (row.app_install_id === appInstallId) {
            return send(200, { ref: row.ref, bound_at: row.bound_at });
          }
        }
        return send(200, { ref: null });
      }

      send(404, { error: "no route" });
    })().catch((err) => send(500, { error: err.message }));
  });

  return new Promise((resolve) => {
    server.listen(0, "127.0.0.1", () => {
      const addr = server.address();
      resolve({
        url: `http://127.0.0.1:${addr.port}`,
        tokens,
        close: () => new Promise((r) => server.close(r)),
      });
    });
  });
}

// --- fake shell ------------------------------------------------------------

function makeFakeShell() {
  const opened = [];
  return {
    opened,
    openExternal: async (url) => {
      opened.push(url);
      return true;
    },
  };
}

// --- temp-dir helper -------------------------------------------------------

function makeTempUserDataDir() {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openswarm-affiliate-test-"));
  return dir;
}

function readJson(p) {
  return JSON.parse(fs.readFileSync(p, "utf8"));
}

function delay(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

// Prefer the actual install_token = call the bind endpoint with it.
async function simulateWelcomePageBind(cloudUrl, installToken, appInstallId) {
  const res = await fetch(`${cloudUrl}/api/install/bind`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ install_token: installToken, app_install_id: appInstallId }),
  });
  return { status: res.status, body: await res.json() };
}

async function mintTokenFromCloud(cloudUrl, ref) {
  const res = await fetch(`${cloudUrl}/api/install/mint`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ ref }),
  });
  const body = await res.json();
  return body.install_token;
}

function appInstallIdFromWelcomeUrl(url) {
  const u = new URL(url);
  return u.searchParams.get("app_install_id");
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

test("first launch: opens welcome URL and binds ref via poll loop", async () => {
  const cloud = await makeMockCloud();
  try {
    const userDataDir = makeTempUserDataDir();
    const shell = makeFakeShell();

    process.env.OPENSWARM_AFFILIATE_LANDING_URL = "https://landing.test";
    process.env.OPENSWARM_AFFILIATE_CLOUD_URL = cloud.url;

    // 1. Pre-mint a token at the cloud as if the user had clicked Download
    //    on the landing page.
    const installToken = await mintTokenFromCloud(cloud.url, "haik-test");

    // 2. Run the desktop's first-launch handshake.
    await affiliateTracking.maybeRunFirstLaunchHandshake({
      shell,
      userDataDir,
      isDev: false,
      isPackaged: true,
    });

    // 3. The shell should have been told to open the welcome URL with the
    //    freshly generated app_install_id.
    assert.equal(shell.opened.length, 1, "exactly one browser open");
    assert.ok(
      shell.opened[0].startsWith("https://landing.test/welcome?app_install_id="),
      "welcome URL has correct prefix",
    );

    const appInstallId = appInstallIdFromWelcomeUrl(shell.opened[0]);
    assert.ok(appInstallId && appInstallId.length > 8, "app_install_id present in URL");

    // 4. install.json on disk now has the app_install_id but no ref yet.
    const stateFile = path.join(userDataDir, "install.json");
    let state = readJson(stateFile);
    assert.equal(state.app_install_id, appInstallId);
    assert.equal(state.ref, null);
    assert.ok(state.first_launch_at > 0);

    // 5. Simulate the welcome page completing the bind.
    const bindResult = await simulateWelcomePageBind(cloud.url, installToken, appInstallId);
    assert.equal(bindResult.status, 200);
    assert.equal(bindResult.body.ref, "haik-test");

    // 6. Wait for the poll loop to pick up the bind. Poll cadence is
    //    20ms × 30 attempts = ~600ms upper bound; we wait up to 1s.
    let final = null;
    for (let i = 0; i < 50; i++) {
      await delay(50);
      final = readJson(stateFile);
      if (final.ref) break;
    }
    assert.equal(final.ref, "haik-test", "ref should have been written by poll loop");
    assert.ok(final.ref_bound_at > 0, "ref_bound_at populated");
  } finally {
    await cloud.close();
  }
});

test("returning launch: no-op when ref already bound", async () => {
  const cloud = await makeMockCloud();
  try {
    const userDataDir = makeTempUserDataDir();
    const shell = makeFakeShell();
    process.env.OPENSWARM_AFFILIATE_CLOUD_URL = cloud.url;

    // Seed install.json as if first launch already happened and a ref
    // was bound a few minutes ago.
    fs.writeFileSync(
      path.join(userDataDir, "install.json"),
      JSON.stringify({
        app_install_id: "preexisting-app-install-id-1234",
        first_launch_at: Date.now() - 5 * 60 * 1000,
        ref: "previously-bound-ref",
        ref_bound_at: Date.now() - 4 * 60 * 1000,
        attempts: 1,
      }),
    );

    await affiliateTracking.maybeRunFirstLaunchHandshake({
      shell,
      userDataDir,
      isDev: false,
      isPackaged: true,
    });

    assert.equal(shell.opened.length, 0, "should NOT pop a browser tab");
    const state = readJson(path.join(userDataDir, "install.json"));
    assert.equal(state.ref, "previously-bound-ref");
  } finally {
    await cloud.close();
  }
});

test("returning launch within grace window: silent re-poll, no second browser pop-up", async () => {
  const cloud = await makeMockCloud();
  try {
    const userDataDir = makeTempUserDataDir();
    const shell = makeFakeShell();
    process.env.OPENSWARM_AFFILIATE_CLOUD_URL = cloud.url;

    // Pre-mint a token + seed install.json as if first launch happened
    // but the user never completed the welcome page handshake yet.
    const appInstallId = "grace-app-install-id-abcdef0123";
    fs.writeFileSync(
      path.join(userDataDir, "install.json"),
      JSON.stringify({
        app_install_id: appInstallId,
        first_launch_at: Date.now() - 10 * 60 * 1000,
        ref: null,
        ref_bound_at: null,
        attempts: 5,
      }),
    );

    const installToken = await mintTokenFromCloud(cloud.url, "grace-ref");
    // Simulate a late welcome bind (user finally clicked through).
    await simulateWelcomePageBind(cloud.url, installToken, appInstallId);

    await affiliateTracking.maybeRunFirstLaunchHandshake({
      shell,
      userDataDir,
      isDev: false,
      isPackaged: true,
    });

    // Specifically NO browser pop-up the second time around.
    assert.equal(shell.opened.length, 0, "no second browser open");

    // Wait for the silent re-poll to pick up the bind.
    const stateFile = path.join(userDataDir, "install.json");
    let state = null;
    for (let i = 0; i < 50; i++) {
      await delay(50);
      state = readJson(stateFile);
      if (state.ref) break;
    }
    assert.equal(state.ref, "grace-ref", "silent re-poll should pick up the bind");
  } finally {
    await cloud.close();
  }
});

test("returning launch outside grace window: skipped entirely", async () => {
  const cloud = await makeMockCloud();
  try {
    const userDataDir = makeTempUserDataDir();
    const shell = makeFakeShell();
    process.env.OPENSWARM_AFFILIATE_CLOUD_URL = cloud.url;

    fs.writeFileSync(
      path.join(userDataDir, "install.json"),
      JSON.stringify({
        app_install_id: "old-app-install-id-1234567890",
        first_launch_at: Date.now() - 7 * 24 * 60 * 60 * 1000,
        ref: null,
        ref_bound_at: null,
        attempts: 12,
      }),
    );

    await affiliateTracking.maybeRunFirstLaunchHandshake({
      shell,
      userDataDir,
      isDev: false,
      isPackaged: true,
    });

    assert.equal(shell.opened.length, 0, "no browser open after grace window");
    // Give the poll loop time to NOT run.
    await delay(200);
    const state = readJson(path.join(userDataDir, "install.json"));
    assert.equal(state.ref, null, "no ref bound");
  } finally {
    await cloud.close();
  }
});

test("dev mode: skipped unless OPENSWARM_AFFILIATE_FORCE=1", async () => {
  const cloud = await makeMockCloud();
  try {
    const userDataDir = makeTempUserDataDir();
    const shell = makeFakeShell();
    process.env.OPENSWARM_AFFILIATE_CLOUD_URL = cloud.url;

    delete process.env.OPENSWARM_AFFILIATE_FORCE;
    await affiliateTracking.maybeRunFirstLaunchHandshake({
      shell,
      userDataDir,
      isDev: true,
      isPackaged: false,
    });
    assert.equal(shell.opened.length, 0, "dev mode skips the handshake");
    assert.ok(!fs.existsSync(path.join(userDataDir, "install.json")), "no state file written");

    process.env.OPENSWARM_AFFILIATE_FORCE = "1";
    try {
      await affiliateTracking.maybeRunFirstLaunchHandshake({
        shell,
        userDataDir,
        isDev: true,
        isPackaged: false,
      });
      assert.equal(shell.opened.length, 1, "force flag opts back in");
    } finally {
      delete process.env.OPENSWARM_AFFILIATE_FORCE;
    }
  } finally {
    await cloud.close();
  }
});

test("install.json write is atomic-ish (temp + rename)", async () => {
  const userDataDir = makeTempUserDataDir();
  affiliateTracking._writeState(userDataDir, { app_install_id: "atomic-test-1234567890", ref: "x" });
  // After write, the temp file shouldn't be left behind.
  const files = fs.readdirSync(userDataDir);
  assert.ok(files.includes("install.json"));
  assert.ok(!files.some((f) => f.endsWith(".tmp")), "no leftover temp file");
});

test("readState returns {} when no install.json exists", () => {
  const userDataDir = makeTempUserDataDir();
  const state = affiliateTracking._readState(userDataDir);
  assert.deepEqual(state, {});
});

test("readState returns {} when install.json is corrupt", () => {
  const userDataDir = makeTempUserDataDir();
  fs.writeFileSync(path.join(userDataDir, "install.json"), "{ not json");
  const state = affiliateTracking._readState(userDataDir);
  assert.deepEqual(state, {});
});

test("poll loop respects max attempts and gives up", async () => {
  // No cloud server at all — every poll attempt fails (ECONNREFUSED).
  const userDataDir = makeTempUserDataDir();
  const shell = makeFakeShell();
  // Point at a port nothing's listening on.
  process.env.OPENSWARM_AFFILIATE_CLOUD_URL = "http://127.0.0.1:1";

  await affiliateTracking.maybeRunFirstLaunchHandshake({
    shell,
    userDataDir,
    isDev: false,
    isPackaged: true,
  });

  // Wait long enough for all attempts to fail. 20ms × 30 = 600ms.
  await delay(900);
  const state = readJson(path.join(userDataDir, "install.json"));
  assert.equal(state.ref, null, "no ref after exhausted polls");
  assert.equal(state.attempts, 30, "all attempts recorded");
});
