// Cloud-only: OAuth callbacks must preserve the URL-selected organization.
//
// A browser session cookie can be pinned to org B while a tab is operating in
// org A via the URL org selector. OAuth redirects leave the console route and
// land on /api/oauth/callback, so the provider state must carry the org selector
// without adding provider-facing query params to redirect_uri.
import { randomBytes } from "node:crypto";

import { expect } from "@effect/vitest";
import { Effect } from "effect";
import type { Page } from "playwright";
import { composePluginApi } from "@executor-js/api/server";
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
import { IntegrationSlug, OAuthClientSlug } from "@executor-js/sdk/shared";
import { serveOAuthTestServer } from "@executor-js/sdk/testing";

import { scenario } from "../src/scenario";
import { Api, Browser, Target } from "../src/services";
import type { Identity } from "../src/target";

const api = composePluginApi([openApiHttpPlugin()] as const);

const unique = (prefix: string) => `${prefix}_${randomBytes(4).toString("hex")}`;

const cookiePair = (response: Response, name: string): string | undefined => {
  for (const header of response.headers.getSetCookie?.() ?? []) {
    if (header.startsWith(`${name}=`)) return header.split(";")[0];
  }
  return undefined;
};

const cookieValue = (pair: string): string => {
  const [, value] = pair.split(/=(.*)/s);
  if (!value) throw new Error("cookie pair has no value");
  return value;
};

const cookieOf = (identity: Identity): string => identity.headers?.cookie ?? "";

const originHeaders = (baseUrl: string) => ({ origin: new URL(baseUrl).origin });

const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

const setWorkosSessionCookie = async (page: Page, baseUrl: string, cookie: string) => {
  await page.context().addCookies([
    {
      name: "wos-session",
      value: cookieValue(cookie),
      url: baseUrl,
    },
  ]);
};

const expectOrgShell = async (
  page: Page,
  org: { readonly name: string; readonly slug: string },
) => {
  await page.waitForURL(
    (url) => url.pathname === `/${org.slug}` || url.pathname === `/${org.slug}/`,
    {
      timeout: 30_000,
    },
  );
  await page.getByRole("button", { name: new RegExp(escapeRegExp(org.name)) }).waitFor();
  await page.getByRole("heading", { name: "Integrations" }).waitFor();
};

const installSameWindowOAuthPopup = async (page: Page) => {
  await page.addInitScript(() => {
    window.open = () =>
      ({
        get closed() {
          return false;
        },
        close() {},
        focus() {},
        location: {
          get href() {
            return window.location.href;
          },
          set href(value: string) {
            window.location.assign(String(value));
          },
        },
      }) as Window;
  });
};

const submitProviderLoginFromPage = async (page: Page): Promise<string> =>
  fetch(page.url(), {
    method: "POST",
    redirect: "manual",
    headers: {
      authorization: `Basic ${Buffer.from("alice:password").toString("base64")}`,
    },
  }).then((response) => {
    const location = response.headers.get("location");
    if (response.status !== 302 || !location) {
      throw new Error(`provider did not return callback location (${response.status})`);
    }
    return new URL(location, page.url()).toString();
  });

const activeOrg = (baseUrl: string, cookie: string) =>
  Effect.promise(async () => {
    const response = await fetch(new URL("/api/auth/me", baseUrl), {
      headers: { cookie },
    });
    if (!response.ok) throw new Error(`/api/auth/me failed (${response.status})`);
    const body = (await response.json()) as {
      organization: { id: string; name: string; slug: string } | null;
    };
    if (!body.organization) throw new Error("identity has no active organization");
    return body.organization;
  });

const createOrganization = (baseUrl: string, cookie: string, name: string) =>
  Effect.promise(async () => {
    const response = await fetch(new URL("/api/auth/create-organization", baseUrl), {
      method: "POST",
      headers: {
        "content-type": "application/json",
        cookie,
        ...originHeaders(baseUrl),
      },
      body: JSON.stringify({ name }),
    });
    if (!response.ok) {
      throw new Error(`/api/auth/create-organization failed (${response.status})`);
    }
    const session = cookiePair(response, "wos-session");
    if (!session) throw new Error("create organization did not refresh the session");
    const org = (await response.json()) as { id: string; name: string; slug: string };
    return { org, session };
  });

const oauthIntegrationSpec = (oauth: {
  readonly authorizationEndpoint: string;
  readonly tokenEndpoint: string;
}) =>
  ({
    spec: {
      kind: "blob" as const,
      value: JSON.stringify({
        openapi: "3.0.3",
        info: { title: "OAuth org scope", version: "1.0.0" },
        paths: {
          "/me": {
            get: {
              operationId: "getMe",
              responses: { "200": { description: "the caller" } },
            },
          },
        },
      }),
    },
    baseUrl: "http://127.0.0.1:59999",
    authenticationTemplate: [
      {
        slug: "oauth",
        kind: "oauth2" as const,
        authorizationUrl: oauth.authorizationEndpoint,
        tokenUrl: oauth.tokenEndpoint,
        scopes: ["read"],
      },
    ],
  }) as const;

scenario(
  "OAuth callback · state-scoped org survives a callback while the session cookie points elsewhere",
  {},
  Effect.gen(function* () {
    const target = yield* Target;
    const { client: makeApiClient } = yield* Api;
    const browser = yield* Browser;
    const oauth = yield* serveOAuthTestServer();

    const identity = yield* target.newIdentity();
    const sessionA = cookieOf(identity);
    const orgA = yield* activeOrg(target.baseUrl, sessionA);

    const { org: orgB, session: sessionB } = yield* createOrganization(
      target.baseUrl,
      sessionA,
      `OAuth Callback Org B ${randomBytes(3).toString("hex")}`,
    );
    expect(orgB.slug, "the test has two distinct org URLs").not.toBe(orgA.slug);

    const client = yield* makeApiClient(api, identity);

    const integration = IntegrationSlug.make(unique("oauthscope"));
    yield* client.openapi.addSpec({
      payload: { ...oauthIntegrationSpec(oauth), slug: integration },
    });

    const clientSlug = OAuthClientSlug.make(unique("oauthc"));
    yield* client.oauth.createClient({
      payload: {
        owner: "org",
        slug: clientSlug,
        authorizationUrl: oauth.authorizationEndpoint,
        tokenUrl: oauth.tokenEndpoint,
        grant: "authorization_code",
        clientId: "test-client",
        clientSecret: "test-secret",
      },
    });

    let callback = new URL("http://invalid.example");
    let providerAuthorizeUrl = new URL("http://invalid.example");
    yield* browser.session(identity, async ({ page, step }) => {
      await installSameWindowOAuthPopup(page);

      await step("Land in the original organization", async () => {
        await page.goto(`/${orgA.slug}`, { waitUntil: "networkidle" });
        await expectOrgShell(page, orgA);
      });

      await step("The browser session is switched to another organization", async () => {
        await setWorkosSessionCookie(page, target.baseUrl, sessionB);
        await page.goto(`/${orgB.slug}`, { waitUntil: "networkidle" });
        await expectOrgShell(page, orgB);
      });

      await step("Start OAuth from the original organization's add-connection flow", async () => {
        // Land WITHOUT ?addAccount=1 and let the org scope settle first. The
        // session cookie still points at org B, so this navigation bounces:
        // first paint under the cookie's org B, OrgSlugGate canonicalizes,
        // then the URL-scoped /account/me settles auth back on org A. A modal
        // auto-opened by the deep link would fire its /api/oauth/clients fetch
        // DURING that transient org-B scope, get org B's empty app list, never
        // refetch, and leave "Connect with OAuth" disabled forever (the flake
        // this step used to have). Waiting for org A's shell before opening
        // the modal makes every modal fetch run under the settled org A scope.
        await page.goto(`/${orgA.slug}/integrations/${String(integration)}`, {
          waitUntil: "networkidle",
        });
        await page.getByRole("button", { name: new RegExp(escapeRegExp(orgA.name)) }).waitFor({
          timeout: 30_000,
        });
        await page.waitForURL((url) => url.pathname.startsWith(`/${orgA.slug}/`), {
          timeout: 30_000,
        });
        await page.getByRole("button", { name: "Add connection" }).click();
        await page.getByRole("heading", { name: /Add connection/ }).waitFor({
          timeout: 30_000,
        });
        // The footer button enables once the registered OAuth app has loaded
        // and is selected; wait for that state instead of racing the click
        // against the apps fetch.
        await page
          .getByRole("button", { name: "Connect with OAuth", disabled: false })
          .waitFor({ timeout: 30_000 });
        // Armed BEFORE the click so the navigation can never outrun the wait.
        const authorizeRequest = page.waitForRequest(
          (request) => {
            const url = new URL(request.url());
            return url.origin === new URL(oauth.issuerUrl).origin && url.pathname === "/authorize";
          },
          { timeout: 30_000 },
        );
        // If the click throws first (e.g. the button never enables), the armed
        // wait would otherwise time out later as an UNHANDLED rejection and
        // pollute the run with phantom errors. Mark it handled here; the
        // `await authorizeRequest` below still surfaces its failure on the
        // success path.
        authorizeRequest.catch(() => {});
        await page.getByRole("button", { name: "Connect with OAuth" }).click();
        providerAuthorizeUrl = new URL((await authorizeRequest).url());
        await page.waitForURL(
          (url) => url.origin === new URL(oauth.issuerUrl).origin && url.pathname === "/login",
          { timeout: 30_000 },
        );
        const redirectUri = new URL(providerAuthorizeUrl.searchParams.get("redirect_uri") ?? "");
        expect(redirectUri.pathname, "the provider redirects to the OAuth callback").toBe(
          "/api/oauth/callback",
        );
        expect(redirectUri.search, "the provider-facing redirect_uri has no org query").toBe("");
        expect(
          providerAuthorizeUrl.searchParams.get("state"),
          "OAuth state is present",
        ).toBeTruthy();
        await page.getByText("OAuth test login").waitFor();
      });

      await step("The provider returns to the OAuth callback", async () => {
        const callbackUrl = await submitProviderLoginFromPage(page);
        callback = new URL(callbackUrl);
        const response = await page.goto(callbackUrl, { waitUntil: "networkidle" });
        expect(response?.status(), "the callback renders its popup result page").toBe(200);
      });

      const body = (await page.locator("body").textContent())?.trim() ?? "";
      expect(
        body,
        "the callback completes in the org where oauth.start stored the session",
      ).toContain("Connected");
      expect(body, "the callback did not fall through to the cookie-pinned org").not.toContain(
        "OAuth session expired or not found",
      );
    });

    expect(
      new URL(providerAuthorizeUrl.searchParams.get("redirect_uri") ?? "").search,
      "the provider-facing redirect_uri remains static",
    ).toBe("");
    expect(
      callback.searchParams.has("executor_org"),
      "the provider callback has no org query",
    ).toBe(false);
  }).pipe(Effect.scoped),
);
