// Cross-cutting auth-layer behaviours that aren't tied to a specific route
// family — see TRI-8743. Soft-deleted projects, revoked keys, expired JWTs,
// cross-env mismatch, force-fallback toggle.
//
// Strategy: pick one representative API-key route
// (GET /api/v1/runs/run_doesnotexist/result) and one representative JWT
// route (POST /api/v1/waitpoints/tokens/<id>/complete) and exercise the
// edge cases against those. The route choice doesn't matter — the
// auth layer is shared across every API route via apiBuilder.server.ts.
// Smoke matrix (api-auth.e2e.test.ts) already covers the trivial
// cases (missing/invalid key, basic JWT pass, soft-deleted project);
// this file adds cases that need explicit fixture setup.

import { generateJWT } from "@trigger.dev/core/v3/jwt";
import { SignJWT } from "jose";
import { describe, expect, it } from "vitest";
import { getTestServer } from "./helpers/sharedTestServer";
import { seedTestEnvironment } from "./helpers/seedTestEnvironment";

describe("Cross-cutting", () => {
  it("shared prisma client can read from the postgres container", async () => {
    const server = getTestServer();
    const count = await server.prisma.user.count();
    expect(count).toBeGreaterThanOrEqual(0);
  });

  // The auth path falls back to RevokedApiKey when a key isn't found
  // in RuntimeEnvironment — letting customers continue to use a key
  // for a configurable grace window after rotation. See
  // models/runtimeEnvironment.server.ts. The grace lookup matches by
  // (apiKey AND expiresAt > now) and rehydrates the env via the FK.
  describe("Revoked API key grace window", () => {
    const route = "/api/v1/runs/run_doesnotexist/result";

    it("revoked key within grace (expiresAt > now): auth passes", async () => {
      const server = getTestServer();
      const { environment } = await seedTestEnvironment(server.prisma);
      // Mint a fresh "rotated" key that doesn't exist on any env, then
      // record it as recently revoked with a future grace expiry.
      const rotatedKey = `tr_dev_rotated_${Math.random().toString(36).slice(2)}`;
      await server.prisma.revokedApiKey.create({
        data: {
          apiKey: rotatedKey,
          runtimeEnvironmentId: environment.id,
          expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // +1 day
        },
      });
      const res = await server.webapp.fetch(route, {
        headers: { Authorization: `Bearer ${rotatedKey}` },
      });
      // Auth passed — the route's resource lookup just doesn't find
      // run_doesnotexist. The point is NOT 401.
      expect(res.status).not.toBe(401);
    });

    it("revoked key past grace (expiresAt < now): 401", async () => {
      const server = getTestServer();
      const { environment } = await seedTestEnvironment(server.prisma);
      const expiredKey = `tr_dev_expired_${Math.random().toString(36).slice(2)}`;
      await server.prisma.revokedApiKey.create({
        data: {
          apiKey: expiredKey,
          runtimeEnvironmentId: environment.id,
          expiresAt: new Date(Date.now() - 60 * 1000), // -1 minute
        },
      });
      const res = await server.webapp.fetch(route, {
        headers: { Authorization: `Bearer ${expiredKey}` },
      });
      expect(res.status).toBe(401);
    });
  });

  // JWT edge cases beyond what the smoke matrix covers (which only
  // checks "wrong key" and "missing scope"). All target the same
  // representative JWT route — the JWT validator is shared across
  // routes via apiBuilder, so coverage here generalises.
  describe("JWT edge cases", () => {
    const route = "/api/v1/waitpoints/tokens/wp_does_not_exist/complete";

    async function postWithJwt(jwt: string) {
      const server = getTestServer();
      return server.webapp.fetch(route, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${jwt}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({}),
      });
    }

    it("JWT with expirationTime in the past: 401", async () => {
      const server = getTestServer();
      const { environment } = await seedTestEnvironment(server.prisma);
      // generateJWT only accepts string expirationTimes (relative, like
      // "15m"). To create a definitively-expired token use jose
      // directly with an absolute past timestamp.
      const secret = new TextEncoder().encode(environment.apiKey);
      const jwt = await new SignJWT({
        pub: true,
        sub: environment.id,
        scopes: ["write:waitpoints"],
      })
        .setIssuer("https://id.trigger.dev")
        .setAudience("https://api.trigger.dev")
        .setProtectedHeader({ alg: "HS256" })
        .setIssuedAt(0)
        .setExpirationTime(1) // 1970-01-01 — definitively expired
        .sign(secret);

      const res = await postWithJwt(jwt);
      expect(res.status).toBe(401);
    });

    it("JWT with pub: false: 401", async () => {
      const server = getTestServer();
      const { environment } = await seedTestEnvironment(server.prisma);
      const jwt = await generateJWT({
        secretKey: environment.apiKey,
        payload: { pub: false, sub: environment.id, scopes: ["write:waitpoints"] },
        expirationTime: "15m",
      });
      // pub: false means "this token isn't meant for client-side use"
      // — the auth layer rejects it for the same-class JWT routes.
      const res = await postWithJwt(jwt);
      expect(res.status).toBe(401);
    });

    it("JWT with no sub claim: 401", async () => {
      const server = getTestServer();
      const { environment } = await seedTestEnvironment(server.prisma);
      const jwt = await generateJWT({
        secretKey: environment.apiKey,
        payload: { pub: true, scopes: ["write:waitpoints"] },
        expirationTime: "15m",
      });
      // No sub claim — auth can't resolve which env the token belongs
      // to, so it must reject. (sub carries the env id.)
      const res = await postWithJwt(jwt);
      expect(res.status).toBe(401);
    });

    it("JWT signed with another env's apiKey (cross-env): 401", async () => {
      const server = getTestServer();
      // env A's id but signed with env B's apiKey — sub-vs-signature
      // mismatch the auth layer must catch.
      const a = await seedTestEnvironment(server.prisma);
      const b = await seedTestEnvironment(server.prisma);
      const jwt = await generateJWT({
        secretKey: b.apiKey, // <-- WRONG key relative to the sub claim
        payload: { pub: true, sub: a.environment.id, scopes: ["write:waitpoints"] },
        expirationTime: "15m",
      });
      const res = await postWithJwt(jwt);
      expect(res.status).toBe(401);
    });

    it("JWT malformed (three parts but invalid base64 in payload): 401", async () => {
      // Three "."-separated parts so the JWT shape gate sees it as a
      // candidate, but the payload segment is non-base64 garbage.
      // Validator must surface this as 401, not 500.
      const malformed = "eyJhbGciOiJIUzI1NiJ9.@@@notbase64@@@.signature";
      const res = await postWithJwt(malformed);
      expect(res.status).toBe(401);
    });
  });

  // The auth layer resolves the JWT's env from the `sub` claim — NOT
  // from the route path. So a JWT for env A hitting a route that
  // fetches a resource from env B should never accidentally see env
  // B's data. Test by minting a JWT for env A and asking for a
  // resource that lives in env B — expect 404 (not 200).
  describe("Cross-environment: JWT auth resolves env from sub, not URL", () => {
    it("env A's JWT cannot read env B's resource: 404", async () => {
      const server = getTestServer();
      const a = await seedTestEnvironment(server.prisma);
      const b = await seedTestEnvironment(server.prisma);

      // Seed a real-ish run row in env B so the route would have
      // something to find IF auth resolved the env from the URL.
      const friendlyId = `run_${Math.random().toString(36).slice(2, 10)}`;
      await server.prisma.taskRun.create({
        data: {
          friendlyId,
          taskIdentifier: "test-task",
          payload: "{}",
          payloadType: "application/json",
          traceId: `trace_${Math.random().toString(36).slice(2)}`,
          spanId: `span_${Math.random().toString(36).slice(2)}`,
          runtimeEnvironmentId: b.environment.id,
          projectId: b.project.id,
          organizationId: b.organization.id,
          engine: "V2",
          status: "COMPLETED_SUCCESSFULLY",
          queue: "task/test-task",
        },
      });

      const jwt = await generateJWT({
        secretKey: a.apiKey,
        payload: { pub: true, sub: a.environment.id, scopes: ["read:runs"] },
        expirationTime: "15m",
      });

      const res = await server.webapp.fetch(`/api/v1/runs/${friendlyId}/result`, {
        headers: { Authorization: `Bearer ${jwt}` },
      });
      // The route resolves runs scoped to the JWT's env (env A). The
      // run lives in env B, so env A's view returns "not found" —
      // critically, NOT 200.
      expect(res.status).not.toBe(200);
      expect([401, 404]).toContain(res.status);
    });
  });
});
