import { beforeAll, describe, expect, test } from "bun:test";
import {
  CLAIM_COOKIE,
  claimFromCookieHeader,
  clientIp,
  hashIp,
  INTENT_COOKIE,
  INTENT_MAX_AGE_SECONDS,
  intentFromCookieHeader,
  newClaimToken,
  openClaim,
  openIntent,
  sealClaim,
  sealIntent,
} from "./claim";

// Fixed, so the signatures below are the ones this file computes and not the
// ones a developer's .env happens to produce.
beforeAll(() => {
  process.env.BETTER_AUTH_SECRET = "test-secret-not-a-real-one";
});

/** A `Cookie:` header, in the shape a browser sends it. */
function cookies(pairs: Record<string, string>): string {
  return Object.entries(pairs)
    .map(([name, value]) => `${name}=${encodeURIComponent(value)}`)
    .join("; ");
}

describe("sealClaim / openClaim", () => {
  test("gives back exactly the token it was handed", () => {
    const token = newClaimToken();
    expect(openClaim(sealClaim(token))).toBe(token);
  });

  test("refuses a token nobody signed, which is the whole point", () => {
    // The shape of a forgery: a plausible token, an invented signature.
    expect(openClaim("someone-elses-token.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")).toBeNull();
    expect(openClaim("someone-elses-token")).toBeNull();
  });

  test("refuses a token whose signature belongs to another token", () => {
    const mine = sealClaim("token-a");
    const signature = mine.slice(mine.lastIndexOf(".") + 1);
    expect(openClaim(`token-b.${signature}`)).toBeNull();
  });

  test("refuses a tampered payload, one character in", () => {
    const sealed = sealClaim("aaaaaaaa");
    expect(openClaim(`b${sealed.slice(1)}`)).toBeNull();
  });

  test("refuses what is not a sealed value at all", () => {
    for (const value of ["", ".", ".sig", "no-dot-here", undefined, null]) {
      expect(openClaim(value)).toBeNull();
    }
  });

  test("survives a signature of the wrong length without throwing", () => {
    // timingSafeEqual throws on mismatched lengths; the guard must come first.
    expect(openClaim("token.short")).toBeNull();
  });

  test("issues a different token every time", () => {
    expect(newClaimToken()).not.toBe(newClaimToken());
  });
});

describe("sealIntent / openIntent", () => {
  test("gives back the address it was handed, lowercased and trimmed", () => {
    expect(openIntent(sealIntent("  Visitor@Example.COM "))).toBe("visitor@example.com");
  });

  test("keeps an address with dots intact, signature separator and all", () => {
    // The cut is on the last dot: an address full of them must survive it.
    expect(openIntent(sealIntent("a.b.c@sub.example.co.uk"))).toBe("a.b.c@sub.example.co.uk");
  });

  test("stops counting once the magic link would have expired", () => {
    const issued = 1_000_000_000_000;
    const sealed = sealIntent("visitor@example.com", issued);
    const life = INTENT_MAX_AGE_SECONDS * 1000;
    expect(openIntent(sealed, issued + life - 1)).toBe("visitor@example.com");
    expect(openIntent(sealed, issued + life)).toBeNull();
    expect(openIntent(sealed, issued + life + 60_000)).toBeNull();
  });

  test("refuses an intent for an address nobody sealed", () => {
    const sealed = sealIntent("victim@example.com");
    const signature = sealed.slice(sealed.lastIndexOf(".") + 1);
    const expiry = sealed.slice(0, sealed.indexOf(":"));
    expect(openIntent(`${expiry}:attacker@evil.test.${signature}`)).toBeNull();
  });

  test("refuses a payload with no expiry in front of it", () => {
    expect(openIntent(sealClaim("visitor@example.com"))).toBeNull();
    expect(openIntent(sealClaim("later:visitor@example.com"))).toBeNull();
  });

  test("does not confuse a claim for an intent, or the reverse", () => {
    expect(openIntent(sealClaim(newClaimToken()))).toBeNull();
    // A claim reads back, but as the raw payload — never as an address.
    expect(openClaim(sealIntent("visitor@example.com"))).toContain(":visitor@example.com");
  });
});

describe("claimFromCookieHeader / intentFromCookieHeader", () => {
  test("finds ours among the browser's other cookies", () => {
    const token = newClaimToken();
    const header = cookies({
      "better-auth.session_token": "irrelevant",
      [CLAIM_COOKIE]: sealClaim(token),
      [INTENT_COOKIE]: sealIntent("visitor@example.com"),
      _ga: "GA1.1.1",
    });
    expect(claimFromCookieHeader(header)).toBe(token);
    expect(intentFromCookieHeader(header)).toBe("visitor@example.com");
  });

  test("reads nothing out of a header that has neither", () => {
    expect(claimFromCookieHeader("_ga=GA1.1.1")).toBeNull();
    expect(intentFromCookieHeader("_ga=GA1.1.1")).toBeNull();
    expect(claimFromCookieHeader(null)).toBeNull();
    expect(intentFromCookieHeader(undefined)).toBeNull();
  });

  test("is not fooled by a cookie whose name merely ends in ours", () => {
    const header = cookies({ [`not_${CLAIM_COOKIE}`]: sealClaim("token") });
    expect(claimFromCookieHeader(header)).toBeNull();
  });

  test("refuses a value the visitor wrote themselves", () => {
    expect(claimFromCookieHeader(`${CLAIM_COOKIE}=someone-elses-token`)).toBeNull();
    expect(intentFromCookieHeader(`${INTENT_COOKIE}=9999999999999:victim@example.com`)).toBeNull();
  });
});

describe("clientIp", () => {
  /** Only the headers a test names; anything else is absent, as in real life. */
  function headers(values: Record<string, string>): Headers {
    return new Headers(values);
  }

  test("believes the edge before anything else", () => {
    const ip = clientIp(
      headers({
        "cf-connecting-ip": "203.0.113.9",
        "x-forwarded-for": "1.2.3.4, 203.0.113.9",
        "x-real-ip": "10.0.0.1",
      }),
    );
    expect(ip).toBe("203.0.113.9");
  });

  test("ignores the leftmost entry, which is the one the client wrote", () => {
    // `curl -H 'X-Forwarded-For: 1.2.3.4'` reaching a proxy that appends.
    expect(clientIp(headers({ "x-forwarded-for": "1.2.3.4, 203.0.113.9" }))).toBe("203.0.113.9");
    expect(clientIp(headers({ "x-forwarded-for": "9.9.9.9, 8.8.8.8, 203.0.113.9" }))).toBe(
      "203.0.113.9",
    );
  });

  test("a forged chain cannot mint a new identity per request", () => {
    const forged = ["1.2.3.4", "5.6.7.8", "9.10.11.12"].map((claimed) =>
      hashIp(clientIp(headers({ "x-forwarded-for": `${claimed}, 203.0.113.9` }))),
    );
    expect(new Set(forged).size).toBe(1);
  });

  test("takes a single entry as it stands, and trims the spacing", () => {
    expect(clientIp(headers({ "x-forwarded-for": "203.0.113.9" }))).toBe("203.0.113.9");
    expect(clientIp(headers({ "x-forwarded-for": " 203.0.113.9 ,  " }))).toBe("203.0.113.9");
  });

  test("falls back to x-real-ip, then to a name rather than an empty key", () => {
    expect(clientIp(headers({ "x-real-ip": "203.0.113.9" }))).toBe("203.0.113.9");
    expect(clientIp(headers({}))).toBe("unknown");
    expect(clientIp(headers({ "x-forwarded-for": " , " }))).toBe("unknown");
  });
});

describe("hashIp", () => {
  test("is stable for an address and different for another", () => {
    expect(hashIp("203.0.113.9")).toBe(hashIp("203.0.113.9"));
    expect(hashIp("203.0.113.9")).not.toBe(hashIp("203.0.113.10"));
  });

  test("does not carry the address it counted", () => {
    expect(hashIp("203.0.113.9")).not.toContain("203.0.113.9");
  });
});
