import { createPost } from "../shared/factories.js";
import { bskyLabeler, userProfile } from "./testData.js";
import {
  TEST_PLUGIN_ID,
  TEST_PLUGIN_MANIFEST,
  TEST_PLUGIN_RAW_MANIFEST,
  getTestPluginSource,
} from "./testPlugin.js";

export class MockServer {
  constructor() {
    this.authorFeeds = new Map();
    this.bookmarks = [];
    this.convos = [];
    this.convoMessages = new Map();
    this.convoMemberLists = new Map();
    this.convoDelays = new Map();
    this.chatLogs = [];
    this.joinLinkPreviews = new Map();
    this.joinLinkJoinedConvos = new Map();
    this.failJoinLinkCodes = new Set();
    this.createRecordCounter = 0;
    this.drafts = [];
    this.draftCounter = 0;
    this.deletedDraftIds = [];
    this.draftLimitReached = false;
    this.interactionPayloads = [];
    this.blobCounter = 0;
    this.messageCounter = 0;
    this.sentMessageRequests = [];
    this.sendMessageFailure = null;
    this.convoForMembersError = null;
    this.typeaheadProfiles = [];
    this.externalLinkCards = new Map();
    this.feedGenerators = [];
    this.feeds = new Map();
    this.hiddenPostUris = [];
    this.labelerSubscriptions = [];
    this.labelerViews = [bskyLabeler];
    this.mutedWords = [];
    this.blockedProfiles = [];
    this.mutedProfiles = [];
    this.contentLabelPrefs = [];
    this.notifications = [];
    this.notificationCursor = undefined;
    this.notificationsDelayMs = 0;
    this.pinnedFeedUris = [];
    this.pinnedListUris = [];
    this.lists = [];
    this.starterPacks = [];
    this.listMembers = new Map();
    this.currentUserListItems = [];
    this.posts = [];
    this.postLikes = new Map();
    this.reportPayloads = [];
    this.postQuotes = new Map();
    this.postReposts = new Map();
    this.postThreadOthers = new Map();
    this.postThreads = new Map();
    this.postThreadDelays = new Map();
    this.postDelays = new Map();
    this.profileFollowers = new Map();
    this.profileFollows = new Map();
    this.knownFollowers = new Map();
    this.profiles = new Map();
    this.savedFeedUris = [];
    this.actorFeeds = new Map();
    this.actorLists = new Map();
    this.searchFeedGenerators = [];
    this.searchPosts = [];
    this.searchPostsBySort = { top: [], latest: [] };
    this.searchProfiles = [];
    this.timelinePosts = [];
    this.timelineDelayMs = 0;
    this.pluginSettings = new Map();
    this.installedPlugins = [];
    // Override the source served for the local test plugin's main.js; defaults
    // to the standard fixture when null.
    this.localPluginSource = null;
    this.registryEntries = [];
    this.liveManifest = null;
    // README markdown served for plugin repos; set to null to simulate a
    // plugin with no README (404).
    this.pluginReadme = "# Remote Themes\n\nA test readme for the plugin.";
    this.tokenRefreshShouldFail = false;
  }

  // Make subsequent OAuth token refreshes fail with a non-retryable error,
  // simulating a stale/revoked refresh token.
  failTokenRefresh() {
    this.tokenRefreshShouldFail = true;
  }

  addAuthorFeedPosts(did, filter, posts) {
    this.authorFeeds.set(`${did}-${filter}`, posts);
  }

  addBookmarks(bookmarks) {
    this.bookmarks.push(...bookmarks);
  }

  addActorFeeds(did, feedGenerators) {
    const existing = this.actorFeeds.get(did) || [];
    this.actorFeeds.set(did, [...existing, ...feedGenerators]);
  }

  addActorLists(did, lists) {
    const existing = this.actorLists.get(did) || [];
    this.actorLists.set(did, [...existing, ...lists]);
  }

  addFeedGenerators(feedGenerators) {
    this.feedGenerators.push(...feedGenerators);
  }

  addLabelerSubscription(did) {
    this.labelerSubscriptions.push(did);
  }

  addLabelerViews(views) {
    this.labelerViews.push(...views);
  }

  addFeedItems(feedUri, posts) {
    this.feeds.set(
      feedUri,
      posts.map((post) => ({ post })),
    );
  }

  addLists(lists) {
    this.lists.push(...lists);
  }

  addStarterPacks(starterPacks) {
    this.starterPacks.push(...starterPacks);
  }

  addListMembers(listUri, profiles) {
    this.listMembers.set(listUri, profiles);
  }

  addCurrentUserListItem({ uri, listUri, subjectDid }) {
    this.currentUserListItems.push({ uri, listUri, subjectDid });
  }

  addListFeedItems(listUri, posts) {
    this.feeds.set(
      listUri,
      posts.map((post) => ({ post })),
    );
  }

  setPinnedFeeds(feedUris) {
    this.pinnedFeedUris = feedUris;
  }

  setPinnedLists(listUris) {
    this.pinnedListUris = listUris;
  }

  setSavedFeeds(feedUris) {
    this.savedFeedUris = feedUris;
  }

  addTimelinePosts(posts) {
    this.timelinePosts.push(...posts);
  }

  // Delays apply to requests made after the setter is called, so a test can
  // let the initial load complete instantly and only slow down a reload.
  setTimelineDelay(delayMs) {
    this.timelineDelayMs = delayMs;
  }

  setNotificationsDelay(delayMs) {
    this.notificationsDelayMs = delayMs;
  }

  addSearchPosts(posts, { sort } = {}) {
    this.searchPosts.push(...posts);
    if (sort) {
      this.searchPostsBySort[sort].push(...posts);
    } else {
      this.searchPostsBySort.top.push(...posts);
      this.searchPostsBySort.latest.push(...posts);
    }
  }

  addSearchProfiles(profiles) {
    this.searchProfiles.push(...profiles);
  }

  addSearchFeedGenerators(feedGenerators) {
    this.searchFeedGenerators.push(...feedGenerators);
  }

  addTypeaheadProfiles(profiles) {
    this.typeaheadProfiles.push(...profiles);
  }

  setConvoForMembersError(errorName) {
    this.convoForMembersError = errorName;
  }

  setExternalLinkCard(url, meta) {
    this.externalLinkCards.set(url, meta);
  }

  addNotifications(notifications, { cursor } = {}) {
    this.notifications.push(...notifications);
    this.notificationCursor = cursor;
  }

  addPosts(posts, { delayMs = 0 } = {}) {
    this.posts.push(...posts);
    for (const post of posts) {
      const preview = post?.embed?.joinLinkPreview;
      if (preview?.code) {
        this.joinLinkPreviews.set(preview.code, preview);
      }
      if (delayMs > 0) {
        this.postDelays.set(post.uri, delayMs);
      }
    }
  }

  addPostLikes(postUri, likes) {
    this.postLikes.set(postUri, likes);
  }

  addPostQuotes(postUri, quotes) {
    this.postQuotes.set(postUri, quotes);
  }

  addPostReposts(postUri, reposts) {
    this.postReposts.set(postUri, reposts);
  }

  setPostThread(postUri, thread, { delayMs = 0 } = {}) {
    this.postThreads.set(postUri, thread);
    if (delayMs > 0) {
      this.postThreadDelays.set(postUri, delayMs);
    }
  }

  setPostThreadOther(postUri, threadOther) {
    this.postThreadOthers.set(postUri, threadOther);
  }

  addProfileFollowers(did, followers) {
    this.profileFollowers.set(did, followers);
  }

  addProfileFollows(did, follows) {
    this.profileFollows.set(did, follows);
  }

  addKnownFollowers(did, followers) {
    this.knownFollowers.set(did, followers);
  }

  addProfile(profile) {
    this.profiles.set(profile.did, profile);
  }

  addConvos(convos) {
    for (const convo of convos) {
      if (!this.convoMessages.has(convo.id)) {
        this.convoMessages.set(convo.id, []);
      }
    }
    this.convos.push(...convos);
  }

  addConvoMembers(convoId, members) {
    this.convoMemberLists.set(convoId, members);
  }

  failConvoMembers({ status = 500, error = "InternalServerError", message }) {
    this.convoMembersFailure = { status, error, message };
  }

  setConvoDelay(convoId, delayMs) {
    this.convoDelays.set(convoId, delayMs);
  }

  // Queued logs are returned (and drained) by the next chat.bsky.convo.getLog
  // poll, simulating events arriving while a conversation is open.
  addChatLogs(logs) {
    this.chatLogs.push(...logs);
  }

  addConvoMessages(convoId, messages) {
    this.convoMessages.set(convoId, messages);
    for (const message of messages) {
      const preview = message?.embed?.joinLinkPreview;
      if (preview?.code) {
        this.joinLinkPreviews.set(preview.code, preview);
      }
    }
  }

  setJoinLinkPreview(code, preview) {
    this.joinLinkPreviews.set(code, preview);
  }

  failJoinLinkRequest(code) {
    this.failJoinLinkCodes.add(code);
  }

  failSendMessage({ status = 400, error = "InvalidRequest", message }) {
    this.sendMessageFailure = { status, error, message };
  }

  setJoinLinkJoinedConvo(code, convo) {
    this.joinLinkJoinedConvos.set(code, convo);
  }

  // Abort all subsequent document navigations (reloads, redirects) so the
  // page stays on the current document. UI states that only exist until a
  // navigation completes (e.g. an in-progress spinner) stay observable.
  async blockNavigations(page) {
    await page.route("**/*", async (route) => {
      if (
        route.request().isNavigationRequest() &&
        route.request().resourceType() === "document"
      ) {
        await route.abort("aborted");
        return;
      }
      await route.fallback();
    });
  }

  async setup(page, { welcomeModalSeen = true } = {}) {
    // Mark the welcome modal as already seen so it doesn't open over
    // logged-out tests; pass welcomeModalSeen: false to test the modal itself.
    if (welcomeModalSeen) {
      await page.addInitScript(() => {
        sessionStorage.setItem("welcome-modal-seen", "true");
      });
    }

    // Stub gif proxy fetches (gif embeds stream from these CDNs).
    await page.route(
      /https:\/\/(t\.gifs\.bsky\.app|.*\.klipy\.com)\/.*/,
      (route) =>
        route.fulfill({
          status: 200,
          contentType: "video/webm",
          body: "",
        }),
    );

    // Stub the YouTube embed player so tests don't hit the network.
    await page.route("https://www.youtube-nocookie.com/**", (route) =>
      route.fulfill({
        status: 200,
        contentType: "text/html",
        body: "<!doctype html><html><body></body></html>",
      }),
    );

    // Stub emoji-picker-element's CDN data fetch so tests don't hit the
    // network. Returns a tiny fixture with one emoji per group the library
    // requires (its assertEmojiData rejects an empty list).
    await page.route("**/emoji-picker-element-data@*/**", (route) =>
      route.fulfill({
        status: 200,
        contentType: "application/json",
        headers: { etag: '"test-emoji-data"' },
        body: JSON.stringify([
          {
            annotation: "party popper",
            emoji: "🎉",
            group: 0,
            order: 1,
            version: 0.6,
            shortcodes: ["tada"],
            tags: ["celebration"],
          },
        ]),
      }),
    );

    // Plugin fixture routes — serve a self-contained test plugin so plugin
    // e2e tests don't depend on plugins-local/.
    await page.route("**/plugins-local/index.json", (route) =>
      route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify([
          {
            id: TEST_PLUGIN_MANIFEST.id,
            name: TEST_PLUGIN_MANIFEST.name,
            author: TEST_PLUGIN_MANIFEST.author,
            description: TEST_PLUGIN_MANIFEST.description,
          },
        ]),
      }),
    );
    await page.route(
      `**/plugins-local/${TEST_PLUGIN_ID}/manifest.json`,
      (route) =>
        route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify(TEST_PLUGIN_RAW_MANIFEST),
        }),
    );
    await page.route(`**/plugins-local/${TEST_PLUGIN_ID}/main.js`, (route) =>
      route.fulfill({
        status: 200,
        contentType: "text/javascript",
        body: this.localPluginSource ?? getTestPluginSource(),
      }),
    );

    // Remote plugin registry routes — serve a fake registry and matching
    // GitHub release assets so flow tests can install remote plugins.
    await page.route(
      "**/improsocial/impro-releases/main/community-plugins.json",
      (route) =>
        route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify(this.registryEntries),
        }),
    );
    await page.route(
      "**/raw.githubusercontent.com/*/*/refs/tags/*/manifest.json",
      (route) => {
        const match = route
          .request()
          .url()
          .match(/refs\/tags\/([^/]+)\/manifest\.json$/);
        const version = match?.[1] ?? "0.0.0";
        const id = this.registryEntries[0]?.id ?? "remote-plugin";
        route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify({
            id,
            name: this.registryEntries[0]?.name ?? "Remote Plugin",
            version,
            description: this.registryEntries[0]?.description,
          }),
        });
      },
    );
    await page.route(
      "**/raw.githubusercontent.com/*/*/refs/tags/*/main.js",
      (route) =>
        route.fulfill({
          status: 200,
          contentType: "text/javascript",
          body: getTestPluginSource(),
        }),
    );
    await page.route(
      "**/raw.githubusercontent.com/*/*/refs/tags/*/styles.css",
      (route) => route.fulfill({ status: 404, body: "Not Found" }),
    );
    await page.route(
      "**/raw.githubusercontent.com/*/*/refs/heads/main/manifest.json",
      (route) => {
        const live = this.liveManifest ?? {
          id: this.registryEntries[0]?.id ?? "remote-plugin",
          name: this.registryEntries[0]?.name ?? "Remote Plugin",
          version: "1.0.0",
        };
        route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify(live),
        });
      },
    );
    // Plugin README, served from the repo's main branch (remote) or the
    // local plugin directory.
    const fulfillReadme = (route) => {
      if (this.pluginReadme === null) {
        route.fulfill({ status: 404, body: "Not Found" });
        return;
      }
      route.fulfill({
        status: 200,
        contentType: "text/markdown",
        body: this.pluginReadme,
      });
    };
    await page.route(
      "**/raw.githubusercontent.com/*/*/refs/heads/main/README.md",
      fulfillReadme,
    );
    await page.route("**/plugins-local/*/README.md", fulfillReadme);

    await page.route("**/.well-known/atproto-did*", (route) =>
      route.fulfill({ status: 404, body: "Not Found" }),
    );

    // OAuth token endpoint used by seeded test sessions. `invalid_request`
    // (not `invalid_grant`) fails immediately without the cross-tab
    // rotated-session recovery delay.
    await page.route("**/oauth/token*", (route) => {
      if (this.tokenRefreshShouldFail) {
        route.fulfill({
          status: 400,
          contentType: "application/json",
          body: JSON.stringify({ error: "invalid_request" }),
        });
        return;
      }
      route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          access_token: "mock-access-token-refreshed",
          refresh_token: "mock-refresh-token-refreshed",
          expires_in: 3600,
        }),
      });
    });

    await page.route("**/xrpc/blue.microcosm.links.getBacklinks*", (route) =>
      route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ records: [], cursor: "" }),
      }),
    );

    await page.route("**/xrpc/com.atproto.server.getSession*", (route) =>
      route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          did: userProfile.did,
          handle: userProfile.handle,
        }),
      }),
    );

    await page.route("**/xrpc/app.bsky.actor.getPreferences*", (route) =>
      route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          preferences: [
            {
              $type: "app.bsky.actor.defs#savedFeedsPrefV2",
              items: [
                {
                  type: "timeline",
                  value: "following",
                  pinned: true,
                  id: "timeline-following",
                },
                ...this.pinnedFeedUris.map((uri) => ({
                  type: "feed",
                  value: uri,
                  pinned: true,
                  id: uri,
                })),
                ...this.pinnedListUris.map((uri) => ({
                  type: "list",
                  value: uri,
                  pinned: true,
                  id: uri,
                })),
                ...this.savedFeedUris.map((uri) => ({
                  type: "feed",
                  value: uri,
                  pinned: false,
                  id: uri,
                })),
              ],
            },
            ...(this.hiddenPostUris.length > 0
              ? [
                  {
                    $type: "app.bsky.actor.defs#improHiddenPostsPref",
                    items: this.hiddenPostUris,
                  },
                ]
              : []),
            ...(this.labelerSubscriptions.length > 0
              ? [
                  {
                    $type: "app.bsky.actor.defs#labelersPref",
                    labelers: this.labelerSubscriptions.map((did) => ({
                      did,
                    })),
                  },
                ]
              : []),
            ...this.contentLabelPrefs,
            ...(this.mutedWords.length > 0
              ? [
                  {
                    $type: "app.bsky.actor.defs#mutedWordsPref",
                    items: this.mutedWords,
                  },
                ]
              : []),
            ...[...this.pluginSettings.entries()].map(([pluginId, data]) => ({
              $type: "app.bsky.actor.defs#improPluginSettingsPref",
              pluginId,
              data,
            })),
            ...(this.installedPlugins.length > 0
              ? [
                  {
                    $type: "app.bsky.actor.defs#improInstalledPluginsPref",
                    plugins: this.installedPlugins,
                  },
                ]
              : []),
          ],
        }),
      }),
    );

    await page.route("**/xrpc/app.bsky.labeler.getServices*", (route) => {
      const url = new URL(route.request().url());
      const requestedDids = url.searchParams.getAll("dids");
      const views =
        requestedDids.length > 0
          ? this.labelerViews.filter((v) =>
              requestedDids.includes(v.creator.did),
            )
          : this.labelerViews;
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ views }),
      });
    });

    await page.route("**/xrpc/app.bsky.notification.getUnreadCount*", (route) =>
      route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          count: this.notifications.filter((n) => !n.isRead).length,
        }),
      }),
    );

    await page.route(
      "**/xrpc/app.bsky.notification.listNotifications*",
      async (route) => {
        if (this.notificationsDelayMs > 0) {
          await new Promise((resolve) =>
            setTimeout(resolve, this.notificationsDelayMs),
          );
        }
        const url = new URL(route.request().url());
        const cursor = url.searchParams.get("cursor") || "";
        const limit = parseInt(url.searchParams.get("limit") || "0", 10);
        const offset = cursor ? parseInt(cursor, 10) : 0;
        const reasons = url.searchParams.getAll("reasons");

        const filteredNotifications =
          reasons.length > 0
            ? this.notifications.filter((n) => reasons.includes(n.reason))
            : this.notifications;

        let notifications, nextCursor;
        if (limit) {
          notifications = filteredNotifications.slice(offset, offset + limit);
          nextCursor =
            offset + limit < filteredNotifications.length
              ? String(offset + limit)
              : "";
        } else {
          notifications = filteredNotifications;
          nextCursor = this.notificationCursor || "";
        }

        return route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify({ notifications, cursor: nextCursor }),
        });
      },
    );

    await page.route("**/xrpc/app.bsky.notification.updateSeen*", (route) => {
      for (const notification of this.notifications) {
        notification.isRead = true;
      }
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: "{}",
      });
    });

    await page.route("**/xrpc/app.bsky.feed.getPosts*", async (route) => {
      const url = new URL(route.request().url());
      const uris = url.searchParams.getAll("uris");
      const delayMs = Math.max(
        0,
        ...uris.map((uri) => this.postDelays.get(uri) ?? 0),
      );
      if (delayMs > 0) {
        await new Promise((resolve) => setTimeout(resolve, delayMs));
      }
      const posts = uris
        .map((uri) => this.posts.find((p) => p.uri === uri))
        .filter(Boolean);
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ posts }),
      });
    });

    await page.route("**/xrpc/chat.bsky.convo.getUnreadCounts*", (route) => {
      const unreadAcceptedConvos = this.convos.filter(
        (convo) => convo.unreadCount > 0 && convo.status !== "request",
      ).length;
      const unreadRequestConvos = this.convos.filter(
        (convo) => convo.unreadCount > 0 && convo.status === "request",
      ).length;
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          unreadAcceptedConvos: Math.min(100, unreadAcceptedConvos),
          unreadRequestConvos: Math.min(100, unreadRequestConvos),
        }),
      });
    });

    await page.route("**/xrpc/chat.bsky.convo.listConvos*", (route) => {
      const url = new URL(route.request().url());
      const readState = url.searchParams.get("readState");
      const status = url.searchParams.get("status");
      const cursor = url.searchParams.get("cursor") || "";
      const limit = parseInt(url.searchParams.get("limit") || "0", 10);
      const offset = cursor ? parseInt(cursor, 10) : 0;
      let allConvos = this.convos;
      if (readState === "unread") {
        allConvos = allConvos.filter((convo) => convo.unreadCount > 0);
      }
      if (status) {
        allConvos = allConvos.filter((convo) => convo.status === status);
      }

      let convos, nextCursor;
      if (limit) {
        convos = allConvos.slice(offset, offset + limit);
        nextCursor =
          offset + limit < allConvos.length ? String(offset + limit) : "";
      } else {
        convos = allConvos;
        nextCursor = "";
      }

      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ convos, cursor: nextCursor }),
      });
    });

    await page.route("**/xrpc/chat.bsky.convo.getConvo*", async (route) => {
      const url = new URL(route.request().url());
      const convoId = url.searchParams.get("convoId");
      const delayMs = this.convoDelays.get(convoId);
      if (delayMs) {
        await new Promise((resolve) => setTimeout(resolve, delayMs));
      }
      const convo = this.convos.find((c) => c.id === convoId);
      if (!convo) {
        return route.fulfill({
          status: 400,
          contentType: "application/json",
          body: JSON.stringify({
            error: "InvalidConvo",
            message: "Conversation not found",
          }),
        });
      }
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ convo }),
      });
    });

    await page.route("**/xrpc/chat.bsky.convo.getConvoMembers*", (route) => {
      if (this.convoMembersFailure) {
        const { status, error, message } = this.convoMembersFailure;
        return route.fulfill({
          status,
          contentType: "application/json",
          body: JSON.stringify({ error, message }),
        });
      }
      const url = new URL(route.request().url());
      const convoId = url.searchParams.get("convoId");
      const convo = this.convos.find((c) => c.id === convoId);
      if (!convo) {
        return route.fulfill({
          status: 400,
          contentType: "application/json",
          body: JSON.stringify({
            error: "InvalidConvo",
            message: "Conversation not found",
          }),
        });
      }
      const allMembers = this.convoMemberLists.get(convoId) ?? convo.members;
      const cursor = url.searchParams.get("cursor") || "";
      const limit = parseInt(url.searchParams.get("limit") || "50", 10);
      const offset = cursor ? parseInt(cursor, 10) : 0;
      const members = allMembers.slice(offset, offset + limit);
      const nextCursor =
        offset + limit < allMembers.length ? String(offset + limit) : "";
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          members,
          ...(nextCursor ? { cursor: nextCursor } : {}),
        }),
      });
    });

    await page.route("**/xrpc/chat.bsky.convo.getMessages*", (route) => {
      const url = new URL(route.request().url());
      const convoId = url.searchParams.get("convoId");
      const messages = this.convoMessages.get(convoId) || [];
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ messages }),
      });
    });

    await page.route("**/xrpc/chat.bsky.convo.sendMessage*", (route) => {
      const body = route.request().postDataJSON();
      this.sentMessageRequests.push(body);
      if (this.sendMessageFailure) {
        const { status, error, message } = this.sendMessageFailure;
        return route.fulfill({
          status,
          contentType: "application/json",
          body: JSON.stringify({ error, message }),
        });
      }
      const msgId = ++this.messageCounter;
      const sentMessage = {
        id: `msg-sent-${msgId}`,
        rev: `rev-sent-${msgId}`,
        text: body?.message?.text || "",
        sender: { did: userProfile.did },
        sentAt: new Date().toISOString(),
      };
      const convo = this.convos.find((c) => c.id === body?.convoId);
      if (convo) {
        convo.lastMessage = {
          $type: "chat.bsky.convo.defs#messageView",
          ...sentMessage,
        };
      }
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify(sentMessage),
      });
    });

    await page.route("**/xrpc/chat.bsky.convo.addReaction*", (route) => {
      const body = route.request().postDataJSON();
      const { convoId, messageId, value } = body || {};
      const messages = this.convoMessages.get(convoId) || [];
      const message = messages.find((m) => m.id === messageId);
      if (message) {
        message.reactions.push({
          createdAt: new Date().toISOString(),
          sender: { did: userProfile.did },
          value,
        });
      }
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ message: message || {} }),
      });
    });

    await page.route(
      "**/xrpc/chat.bsky.group.getJoinLinkPreviews*",
      (route) => {
        const url = new URL(route.request().url());
        const codes = url.searchParams.getAll("codes");
        const joinLinkPreviews = codes
          .map((code) => this.joinLinkPreviews.get(code))
          .filter(Boolean);
        return route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify({ joinLinkPreviews }),
        });
      },
    );

    await page.route("**/xrpc/chat.bsky.group.requestJoin*", (route) => {
      const body = route.request().postDataJSON();
      const code = body?.code;
      if (this.failJoinLinkCodes.has(code)) {
        return route.fulfill({
          status: 500,
          contentType: "application/json",
          body: JSON.stringify({ error: "ServerError" }),
        });
      }
      const preview = this.joinLinkPreviews.get(code);
      const joinedConvo =
        preview?.requireApproval === false
          ? this.joinLinkJoinedConvos.get(code)
          : null;
      if (preview?.$type === "chat.bsky.group.defs#joinLinkPreviewView") {
        this.joinLinkPreviews.set(code, {
          ...preview,
          ...(joinedConvo ? { convo: joinedConvo } : {}),
          viewer: {
            ...(preview.viewer ?? {}),
            ...(joinedConvo ? {} : { requestedAt: new Date().toISOString() }),
          },
        });
      }
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify(
          joinedConvo
            ? { status: "joined", convo: joinedConvo }
            : { status: "pending" },
        ),
      });
    });

    await page.route("**/xrpc/chat.bsky.convo.removeReaction*", (route) => {
      const body = route.request().postDataJSON();
      const { convoId, messageId, value } = body || {};
      const messages = this.convoMessages.get(convoId) || [];
      const message = messages.find((m) => m.id === messageId);
      if (message) {
        message.reactions = message.reactions.filter(
          (r) => !(r.value === value && r.sender.did === userProfile.did),
        );
      }
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ message: message || {} }),
      });
    });

    await page.route("**/xrpc/chat.bsky.convo.updateRead*", (route) => {
      const body = route.request().postDataJSON();
      const convoId = body?.convoId;
      const convo = this.convos.find((c) => c.id === convoId);
      if (convo) {
        convo.unreadCount = 0;
      }
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ convo: convo || {} }),
      });
    });

    await page.route("**/xrpc/chat.bsky.convo.getLog*", (route) => {
      const logs = this.chatLogs.splice(0, this.chatLogs.length);
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ logs, cursor: "" }),
      });
    });

    await page.route("**/xrpc/chat.bsky.convo.acceptConvo*", (route) => {
      const body = route.request().postDataJSON();
      const convo = this.convos.find((c) => c.id === body?.convoId);
      if (convo) {
        convo.status = "accepted";
      }
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ rev: "rev-accepted" }),
      });
    });

    await page.route("**/xrpc/chat.bsky.convo.leaveConvo*", (route) => {
      const body = route.request().postDataJSON();
      this.convos = this.convos.filter((c) => c.id !== body?.convoId);
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ rev: "rev-left" }),
      });
    });

    await page.route(
      "**/xrpc/chat.bsky.convo.getConvoAvailability*",
      (route) => {
        const url = new URL(route.request().url());
        const members = url.searchParams.getAll("members");
        const otherDid = members.find((m) => m !== userProfile.did);
        const profile = this.profiles.get(otherDid);
        return route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify({ canChat: profile?.canChat ?? false }),
        });
      },
    );

    await page.route("**/xrpc/chat.bsky.convo.getConvoForMembers*", (route) => {
      if (this.convoForMembersError) {
        return route.fulfill({
          status: 400,
          contentType: "application/json",
          body: JSON.stringify({
            error: this.convoForMembersError,
            message: this.convoForMembersError,
          }),
        });
      }
      const url = new URL(route.request().url());
      const members = url.searchParams.getAll("members");
      const otherDid = members.find((m) => m !== userProfile.did);
      // Find existing convo with this member
      const existingConvo = this.convos.find((c) =>
        c.members.some((m) => m.did === otherDid),
      );
      if (existingConvo) {
        return route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify({ convo: existingConvo }),
        });
      }
      // Create a new convo
      const profile = this.profiles.get(otherDid);
      const newConvo = {
        id: `convo-new-${++this.messageCounter}`,
        rev: `rev-new-${this.messageCounter}`,
        members: [userProfile, profile || { did: otherDid }],
        status: "accepted",
        unreadCount: 0,
        lastMessage: undefined,
      };
      this.convos.push(newConvo);
      this.convoMessages.set(newConvo.id, []);
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ convo: newConvo }),
      });
    });

    await page.route("**/xrpc/app.bsky.graph.muteActor*", (route) => {
      const body = route.request().postDataJSON();
      const actor = body?.actor;
      const profile = this.profiles.get(actor);
      if (profile) {
        profile.viewer = { ...profile.viewer, muted: true };
      }
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: "{}",
      });
    });

    await page.route("**/xrpc/app.bsky.graph.unmuteActor*", (route) => {
      const body = route.request().postDataJSON();
      const actor = body?.actor;
      const profile = this.profiles.get(actor);
      if (profile) {
        profile.viewer = { ...profile.viewer, muted: false };
      }
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: "{}",
      });
    });

    await page.route("**/xrpc/app.bsky.graph.muteActorList*", (route) => {
      const body = route.request().postDataJSON();
      const listUri = body?.list;
      const list = this.lists.find((l) => l.uri === listUri);
      if (list) {
        list.viewer = { ...list.viewer, muted: true };
      }
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: "{}",
      });
    });

    await page.route("**/xrpc/app.bsky.graph.unmuteActorList*", (route) => {
      const body = route.request().postDataJSON();
      const listUri = body?.list;
      const list = this.lists.find((l) => l.uri === listUri);
      if (list) {
        list.viewer = { ...list.viewer, muted: false };
      }
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: "{}",
      });
    });

    await page.route(
      "**/xrpc/app.bsky.notification.putActivitySubscription*",
      (route) => {
        const body = route.request().postDataJSON();
        const subject = body?.subject;
        const activitySubscription = body?.activitySubscription;
        const profile = this.profiles.get(subject);
        if (profile) {
          profile.viewer = { ...profile.viewer, activitySubscription };
        }
        return route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify({ subject, activitySubscription }),
        });
      },
    );

    await page.route("**/xrpc/app.bsky.feed.getActorLikes*", (route) => {
      const url = new URL(route.request().url());
      const actor = url.searchParams.get("actor");
      const cursor = url.searchParams.get("cursor") || "";
      const limit = parseInt(url.searchParams.get("limit") || "0", 10);
      const offset = cursor ? parseInt(cursor, 10) : 0;
      const allPosts = this.authorFeeds.get(`${actor}-likes`) || [];

      let posts, nextCursor;
      if (limit) {
        posts = allPosts.slice(offset, offset + limit);
        nextCursor =
          offset + limit < allPosts.length ? String(offset + limit) : "";
      } else {
        posts = allPosts;
        nextCursor = "";
      }

      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          feed: posts.map((post) => ({ post })),
          cursor: nextCursor,
        }),
      });
    });

    await page.route("**/xrpc/app.bsky.feed.getAuthorFeed*", (route) => {
      const url = new URL(route.request().url());
      const actor = url.searchParams.get("actor");
      const filter = url.searchParams.get("filter") || "";
      const cursor = url.searchParams.get("cursor") || "";
      const limit = parseInt(url.searchParams.get("limit") || "0", 10);
      const offset = cursor ? parseInt(cursor, 10) : 0;
      const allPosts = this.authorFeeds.get(`${actor}-${filter}`) || [];

      let posts, nextCursor;
      if (limit) {
        posts = allPosts.slice(offset, offset + limit);
        nextCursor =
          offset + limit < allPosts.length ? String(offset + limit) : "";
      } else {
        posts = allPosts;
        nextCursor = "";
      }

      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          feed: posts.map((post) => ({ post })),
          cursor: nextCursor,
        }),
      });
    });

    await page.route("**/xrpc/app.bsky.actor.getProfile*", (route) => {
      const url = new URL(route.request().url());
      const actor = url.searchParams.get("actor");
      const profile = this.profiles.get(actor) || userProfile;
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify(profile),
      });
    });

    await page.route("**/xrpc/app.bsky.actor.getProfiles*", (route) => {
      const url = new URL(route.request().url());
      const actors = url.searchParams.getAll("actors");
      const profiles = actors
        .map((actor) => this.profiles.get(actor))
        .filter(Boolean);
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ profiles }),
      });
    });

    await page.route("**/xrpc/app.bsky.bookmark.getBookmarks*", (route) =>
      route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          bookmarks: this.bookmarks.map((post) => ({ item: post })),
        }),
      }),
    );

    await page.route("**/xrpc/app.bsky.bookmark.createBookmark*", (route) => {
      const body = route.request().postDataJSON();
      const postUri = body?.uri;
      const allPosts = [
        ...this.timelinePosts,
        ...this.bookmarks,
        ...this.searchPosts,
        ...this.posts,
      ];
      const post = allPosts.find((p) => p.uri === postUri);
      if (post) {
        post.viewer.bookmarked = true;
        if (!this.bookmarks.includes(post)) {
          this.bookmarks.push(post);
        }
      }
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: "{}",
      });
    });

    await page.route("**/xrpc/app.bsky.bookmark.deleteBookmark*", (route) => {
      const body = route.request().postDataJSON();
      const postUri = body?.uri;
      const idx = this.bookmarks.findIndex((p) => p.uri === postUri);
      if (idx !== -1) {
        this.bookmarks[idx].viewer.bookmarked = false;
        this.bookmarks.splice(idx, 1);
      }
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: "{}",
      });
    });

    await page.route("**/xrpc/app.bsky.draft.getDrafts*", (route) =>
      route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ drafts: this.drafts }),
      }),
    );

    await page.route("**/xrpc/app.bsky.draft.createDraft*", (route) => {
      if (this.draftLimitReached) {
        return route.fulfill({
          status: 400,
          contentType: "application/json",
          body: JSON.stringify({
            error: "DraftLimitReached",
            message: "Draft limit reached",
          }),
        });
      }
      const body = route.request().postDataJSON();
      const id = `draft-${++this.draftCounter}`;
      const now = new Date().toISOString();
      this.drafts.unshift({
        id,
        createdAt: now,
        updatedAt: now,
        draft: body.draft,
      });
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ id }),
      });
    });

    await page.route("**/xrpc/app.bsky.draft.updateDraft*", (route) => {
      const body = route.request().postDataJSON();
      const existing = this.drafts.find(
        (draftView) => draftView.id === body.draft.id,
      );
      if (existing) {
        existing.draft = body.draft.draft;
        existing.updatedAt = new Date().toISOString();
      }
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: "{}",
      });
    });

    await page.route("**/xrpc/app.bsky.draft.deleteDraft*", (route) => {
      const body = route.request().postDataJSON();
      this.deletedDraftIds.push(body.id);
      this.drafts = this.drafts.filter((draftView) => draftView.id !== body.id);
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: "{}",
      });
    });

    await page.route("**/xrpc/app.bsky.actor.searchActors*", (route) => {
      const url = new URL(route.request().url());
      const cursor = url.searchParams.get("cursor") || "";
      const limit = parseInt(url.searchParams.get("limit") || "0", 10);
      const offset = cursor ? parseInt(cursor, 10) : 0;

      let actors, nextCursor;
      if (limit) {
        actors = this.searchProfiles.slice(offset, offset + limit);
        nextCursor =
          offset + limit < this.searchProfiles.length
            ? String(offset + limit)
            : "";
      } else {
        actors = this.searchProfiles;
        nextCursor = "";
      }

      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ actors, cursor: nextCursor }),
      });
    });

    await page.route(
      "**/xrpc/app.bsky.unspecced.getPopularFeedGenerators*",
      (route) => {
        const url = new URL(route.request().url());
        const cursor = url.searchParams.get("cursor") || "";
        const limit = parseInt(url.searchParams.get("limit") || "0", 10);
        const offset = cursor ? parseInt(cursor, 10) : 0;

        let feeds, nextCursor;
        if (limit) {
          feeds = this.searchFeedGenerators.slice(offset, offset + limit);
          nextCursor =
            offset + limit < this.searchFeedGenerators.length
              ? String(offset + limit)
              : "";
        } else {
          feeds = this.searchFeedGenerators;
          nextCursor = "";
        }

        return route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify({ feeds, cursor: nextCursor }),
        });
      },
    );

    await page.route("**/xrpc/app.bsky.feed.getActorFeeds*", (route) => {
      const url = new URL(route.request().url());
      const actor = url.searchParams.get("actor") || "";
      const cursor = url.searchParams.get("cursor") || "";
      const limit = parseInt(url.searchParams.get("limit") || "0", 10);
      const offset = cursor ? parseInt(cursor, 10) : 0;
      const allFeeds = this.actorFeeds.get(actor) || [];

      let feeds, nextCursor;
      if (limit) {
        feeds = allFeeds.slice(offset, offset + limit);
        nextCursor =
          offset + limit < allFeeds.length ? String(offset + limit) : "";
      } else {
        feeds = allFeeds;
        nextCursor = "";
      }

      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ feeds, cursor: nextCursor }),
      });
    });

    await page.route("**/xrpc/app.bsky.feed.searchPosts*", (route) => {
      const url = new URL(route.request().url());
      const cursor = url.searchParams.get("cursor") || "";
      const limit = parseInt(url.searchParams.get("limit") || "0", 10);
      const offset = cursor ? parseInt(cursor, 10) : 0;
      const sort = url.searchParams.get("sort") || "top";
      const sortedPosts = this.searchPostsBySort[sort] ?? this.searchPosts;

      let posts, nextCursor;
      if (limit) {
        posts = sortedPosts.slice(offset, offset + limit);
        nextCursor =
          offset + limit < sortedPosts.length ? String(offset + limit) : "";
      } else {
        posts = sortedPosts;
        nextCursor = "";
      }

      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ posts, cursor: nextCursor }),
      });
    });

    await page.route("**/xrpc/app.bsky.feed.getTimeline*", async (route) => {
      if (this.timelineDelayMs > 0) {
        await new Promise((resolve) =>
          setTimeout(resolve, this.timelineDelayMs),
        );
      }
      const url = new URL(route.request().url());
      const cursor = url.searchParams.get("cursor") || "";
      const limit = parseInt(url.searchParams.get("limit") || "0", 10);
      const offset = cursor ? parseInt(cursor, 10) : 0;

      const blockedDids = new Set();
      for (const [did, profile] of this.profiles) {
        if (profile.viewer?.blocking || profile.viewer?.muted) {
          blockedDids.add(did);
        }
      }
      const allPosts = this.timelinePosts.filter(
        (post) => !blockedDids.has(post.author?.did),
      );

      let posts, nextCursor;
      if (limit) {
        posts = allPosts.slice(offset, offset + limit);
        nextCursor =
          offset + limit < allPosts.length ? String(offset + limit) : "";
      } else {
        posts = allPosts;
        nextCursor = "";
      }

      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          feed: posts.map((post) => ({ post })),
          cursor: nextCursor,
        }),
      });
    });

    await page.route("**/xrpc/app.bsky.feed.getListFeed*", (route) => {
      const url = new URL(route.request().url());
      const listUri = url.searchParams.get("list");
      const feed = this.feeds.get(listUri) || [];
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ feed, cursor: "" }),
      });
    });

    // Order matters: Playwright checks routes in LIFO order, so register
    // the most general pattern first (checked last) and most specific last.
    await page.route("**/xrpc/app.bsky.feed.getFeed*", (route) => {
      const url = new URL(route.request().url());
      const feedUri = url.searchParams.get("feed");
      const feed = this.feeds.get(feedUri) || [];
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ feed, cursor: "" }),
      });
    });

    await page.route("**/xrpc/app.bsky.feed.getFeedGenerator*", (route) => {
      const url = new URL(route.request().url());
      const feedUri = url.searchParams.get("feed");
      const generator = this.feedGenerators.find((g) => g.uri === feedUri);
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ view: generator || {} }),
      });
    });

    await page.route("**/xrpc/app.bsky.feed.getFeedGenerators*", (route) => {
      const url = new URL(route.request().url());
      const feedUris = url.searchParams.getAll("feeds");
      const feeds = feedUris.map(
        (uri) => this.feedGenerators.find((g) => g.uri === uri) || {},
      );
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ feeds }),
      });
    });

    await page.route("**/xrpc/app.bsky.graph.getList*", (route) => {
      const url = new URL(route.request().url());
      const listUri = url.searchParams.get("list");
      const list = this.lists.find((l) => l.uri === listUri) || {};
      const members = this.listMembers.get(listUri) || [];
      const items = members.map((profile) => ({ subject: profile }));
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ list, items, cursor: "" }),
      });
    });

    await page.route("https://ogcard.cdn.bsky.app/**", (route) => {
      return route.fulfill({
        status: 200,
        contentType: "image/png",
        body: Buffer.from(
          "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
          "base64",
        ),
      });
    });

    await page.route("**/xrpc/app.bsky.graph.getStarterPack?*", (route) => {
      const url = new URL(route.request().url());
      const starterPackUri = url.searchParams.get("starterPack");
      const starterPack =
        this.starterPacks.find((s) => s.uri === starterPackUri) || {};
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ starterPack }),
      });
    });

    await page.route("**/xrpc/app.bsky.graph.getLists*", (route) => {
      const url = new URL(route.request().url());
      const actor = url.searchParams.get("actor") || "";
      const cursor = url.searchParams.get("cursor") || "";
      const limit = parseInt(url.searchParams.get("limit") || "0", 10);
      const offset = cursor ? parseInt(cursor, 10) : 0;
      const allLists = this.actorLists.get(actor) || [];

      let lists, nextCursor;
      if (limit) {
        lists = allLists.slice(offset, offset + limit);
        nextCursor =
          offset + limit < allLists.length ? String(offset + limit) : "";
      } else {
        lists = allLists;
        nextCursor = "";
      }

      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ lists, cursor: nextCursor }),
      });
    });

    await page.route(
      "**/xrpc/app.bsky.graph.getListsWithMembership*",
      (route) => {
        const url = new URL(route.request().url());
        const actor = url.searchParams.get("actor");
        const limit = parseInt(url.searchParams.get("limit") || "50", 10);
        const cursorParam = url.searchParams.get("cursor") || "";
        const start = cursorParam ? parseInt(cursorParam, 10) : 0;
        const lists = this.actorLists.get(userProfile.did) || [];
        const page = lists.slice(start, start + limit);
        const listsWithMembership = page.map((list) => {
          const item = this.currentUserListItems.find(
            (entry) => entry.listUri === list.uri && entry.subjectDid === actor,
          );
          return item
            ? { list, listItem: { uri: item.uri, subject: actor } }
            : { list };
        });
        const nextStart = start + page.length;
        const cursor = nextStart < lists.length ? String(nextStart) : "";
        return route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify({ listsWithMembership, cursor }),
        });
      },
    );

    await page.route("**/xrpc/app.bsky.feed.getLikes*", (route) => {
      const url = new URL(route.request().url());
      const uri = url.searchParams.get("uri");
      const likes = this.postLikes.get(uri) || [];
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ likes, cursor: "" }),
      });
    });

    await page.route("**/xrpc/app.bsky.feed.getQuotes*", (route) => {
      const url = new URL(route.request().url());
      const uri = url.searchParams.get("uri");
      const posts = this.postQuotes.get(uri) || [];
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ posts, cursor: "" }),
      });
    });

    await page.route("**/xrpc/app.bsky.feed.getRepostedBy*", (route) => {
      const url = new URL(route.request().url());
      const uri = url.searchParams.get("uri");
      const repostedBy = this.postReposts.get(uri) || [];
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ repostedBy, cursor: "" }),
      });
    });

    await page.route("**/xrpc/app.bsky.feed.getPostThread*", async (route) => {
      const url = new URL(route.request().url());
      const uri = url.searchParams.get("uri");
      const customThread = this.postThreads.get(uri);
      if (customThread) {
        const delayMs = this.postThreadDelays.get(uri);
        if (delayMs) {
          await new Promise((resolve) => setTimeout(resolve, delayMs));
        }
        return route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify({ thread: customThread }),
        });
      }
      const allPosts = [
        ...this.timelinePosts,
        ...this.bookmarks,
        ...this.searchPosts,
        ...this.posts,
      ];
      const post = allPosts.find((p) => p.uri === uri);
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          thread: {
            $type: "app.bsky.feed.defs#threadViewPost",
            post: post || {},
            replies: [],
          },
        }),
      });
    });

    await page.route(
      "**/xrpc/app.bsky.unspecced.getPostThreadOtherV2*",
      (route) => {
        const url = new URL(route.request().url());
        const anchor = url.searchParams.get("anchor");
        const customThreadOther = this.postThreadOthers.get(anchor);
        return route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify({ thread: customThreadOther || [] }),
        });
      },
    );

    await page.route("**/xrpc/app.bsky.graph.getFollowers*", (route) => {
      const url = new URL(route.request().url());
      const actor = url.searchParams.get("actor");
      const cursor = url.searchParams.get("cursor") || "";
      const limit = parseInt(url.searchParams.get("limit") || "0", 10);
      const offset = cursor ? parseInt(cursor, 10) : 0;
      const allFollowers = this.profileFollowers.get(actor) || [];

      let followers, nextCursor;
      if (limit) {
        followers = allFollowers.slice(offset, offset + limit);
        nextCursor =
          offset + limit < allFollowers.length ? String(offset + limit) : "";
      } else {
        followers = allFollowers;
        nextCursor = "";
      }

      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ followers, cursor: nextCursor }),
      });
    });

    await page.route("**/xrpc/app.bsky.graph.getKnownFollowers*", (route) => {
      const url = new URL(route.request().url());
      const actor = url.searchParams.get("actor");
      const cursor = url.searchParams.get("cursor") || "";
      const limit = parseInt(url.searchParams.get("limit") || "0", 10);
      const offset = cursor ? parseInt(cursor, 10) : 0;
      const allFollowers = this.knownFollowers.get(actor) || [];

      let followers, nextCursor;
      if (limit) {
        followers = allFollowers.slice(offset, offset + limit);
        nextCursor =
          offset + limit < allFollowers.length ? String(offset + limit) : "";
      } else {
        followers = allFollowers;
        nextCursor = "";
      }

      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ followers, cursor: nextCursor }),
      });
    });

    await page.route("**/xrpc/app.bsky.graph.getFollows*", (route) => {
      const url = new URL(route.request().url());
      const actor = url.searchParams.get("actor");
      const cursor = url.searchParams.get("cursor") || "";
      const limit = parseInt(url.searchParams.get("limit") || "0", 10);
      const offset = cursor ? parseInt(cursor, 10) : 0;
      const allFollows = this.profileFollows.get(actor) || [];

      let follows, nextCursor;
      if (limit) {
        follows = allFollows.slice(offset, offset + limit);
        nextCursor =
          offset + limit < allFollows.length ? String(offset + limit) : "";
      } else {
        follows = allFollows;
        nextCursor = "";
      }

      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ follows, cursor: nextCursor }),
      });
    });

    await page.route("**/xrpc/app.bsky.graph.getBlocks*", (route) => {
      const url = new URL(route.request().url());
      const cursor = url.searchParams.get("cursor") || "";
      const limit = parseInt(url.searchParams.get("limit") || "0", 10);
      const offset = cursor ? parseInt(cursor, 10) : 0;

      let blocks, nextCursor;
      if (limit) {
        blocks = this.blockedProfiles.slice(offset, offset + limit);
        nextCursor =
          offset + limit < this.blockedProfiles.length
            ? String(offset + limit)
            : "";
      } else {
        blocks = this.blockedProfiles;
        nextCursor = "";
      }

      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ blocks, cursor: nextCursor }),
      });
    });

    await page.route("**/xrpc/app.bsky.graph.getMutes*", (route) => {
      const url = new URL(route.request().url());
      const cursor = url.searchParams.get("cursor") || "";
      const limit = parseInt(url.searchParams.get("limit") || "0", 10);
      const offset = cursor ? parseInt(cursor, 10) : 0;

      let mutes, nextCursor;
      if (limit) {
        mutes = this.mutedProfiles.slice(offset, offset + limit);
        nextCursor =
          offset + limit < this.mutedProfiles.length
            ? String(offset + limit)
            : "";
      } else {
        mutes = this.mutedProfiles;
        nextCursor = "";
      }

      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ mutes, cursor: nextCursor }),
      });
    });

    await page.route("**/xrpc/com.atproto.identity.resolveHandle*", (route) => {
      const url = new URL(route.request().url());
      const handle = url.searchParams.get("handle");
      const allPosts = [
        ...this.timelinePosts,
        ...this.bookmarks,
        ...this.searchPosts,
        ...this.posts,
      ];
      const postAuthor = allPosts.find(
        (p) => p.author?.handle === handle,
      )?.author;
      const generator = this.feedGenerators.find(
        (g) => g.creator.handle === handle,
      );
      const list = this.lists.find((l) => l.creator?.handle === handle);
      const starterPack = this.starterPacks.find(
        (s) => s.creator?.handle === handle,
      );
      const profileEntry = [...this.profiles.values()].find(
        (p) => p.handle === handle,
      );
      const did =
        postAuthor?.did ||
        generator?.creator?.did ||
        list?.creator?.did ||
        starterPack?.creator?.did ||
        profileEntry?.did;
      if (!did) {
        return route.fulfill({
          status: 404,
          body: JSON.stringify({ error: "NotFound" }),
        });
      }
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ did }),
      });
    });

    await page.route("**/xrpc/com.atproto.repo.deleteRecord*", (route) => {
      const body = route.request().postDataJSON();
      const collection = body?.collection;
      const rkey = body?.rkey;

      if (collection === "app.bsky.feed.like") {
        const feedKey = `${userProfile.did}-likes`;
        const likes = this.authorFeeds.get(feedKey) || [];
        this.authorFeeds.set(
          feedKey,
          likes.filter((p) => p.viewer?.like?.split("/").pop() !== rkey),
        );
      }

      if (collection === "app.bsky.feed.repost") {
        const allPosts = [
          ...this.timelinePosts,
          ...this.bookmarks,
          ...this.searchPosts,
          ...this.posts,
        ];
        for (const post of allPosts) {
          if (
            post.viewer?.repost &&
            post.viewer.repost.split("/").pop() === rkey
          ) {
            delete post.viewer.repost;
            post.repostCount = Math.max(0, (post.repostCount || 0) - 1);
            const feedKey = `${userProfile.did}-posts_and_author_threads`;
            const existing = this.authorFeeds.get(feedKey) || [];
            this.authorFeeds.set(
              feedKey,
              existing.filter((p) => p !== post),
            );
            break;
          }
        }
      }

      if (collection === "app.bsky.graph.follow") {
        for (const [did, profile] of this.profiles) {
          if (
            profile.viewer?.following &&
            profile.viewer.following.split("/").pop() === rkey
          ) {
            profile.viewer = { ...profile.viewer, following: undefined };
            if (
              profile.followersCount !== undefined &&
              profile.followersCount > 0
            ) {
              profile.followersCount--;
            }
            const follows = this.profileFollows.get(userProfile.did) || [];
            this.profileFollows.set(
              userProfile.did,
              follows.filter((p) => p.did !== did),
            );
            break;
          }
        }
      }

      if (collection === "app.bsky.graph.block") {
        for (const [, profile] of this.profiles) {
          if (
            profile.viewer?.blocking &&
            profile.viewer.blocking.split("/").pop() === rkey
          ) {
            profile.viewer = { ...profile.viewer, blocking: undefined };
            break;
          }
        }
      }

      if (collection === "app.bsky.graph.listblock") {
        for (const list of this.lists) {
          if (
            list.viewer?.blocked &&
            list.viewer.blocked.split("/").pop() === rkey
          ) {
            list.viewer = { ...list.viewer, blocked: undefined };
            break;
          }
        }
      }

      if (collection === "app.bsky.feed.post") {
        const postUri = `at://${userProfile.did}/${collection}/${rkey}`;
        this.timelinePosts = this.timelinePosts.filter(
          (p) => p.uri !== postUri,
        );
        this.posts = this.posts.filter((p) => p.uri !== postUri);
        for (const [key, posts] of this.authorFeeds) {
          this.authorFeeds.set(
            key,
            posts.filter((p) => p.uri !== postUri),
          );
        }
      }

      if (collection === "app.bsky.graph.listitem") {
        const itemUri = `at://${userProfile.did}/${collection}/${rkey}`;
        this.currentUserListItems = this.currentUserListItems.filter(
          (item) => item.uri !== itemUri,
        );
      }

      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: "{}",
      });
    });

    await page.route("**/xrpc/com.atproto.repo.listRecords*", (route) => {
      const url = new URL(route.request().url());
      const collection = url.searchParams.get("collection");
      if (collection === "app.bsky.graph.listitem") {
        const records = this.currentUserListItems.map((item) => ({
          uri: item.uri,
          cid: `bafyrei${item.uri.split("/").pop()}`,
          value: {
            $type: "app.bsky.graph.listitem",
            subject: item.subjectDid,
            list: item.listUri,
            createdAt: "2025-01-01T00:00:00.000Z",
          },
        }));
        return route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify({ records, cursor: "" }),
        });
      }
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ records: [], cursor: "" }),
      });
    });

    await page.route("**/xrpc/com.atproto.repo.createRecord*", (route) => {
      const body = route.request().postDataJSON();
      const collection = body?.collection;
      const rkey = `rkey-${++this.createRecordCounter}`;
      const uri = `at://${userProfile.did}/${collection}/${rkey}`;
      const cid = `bafyrei${rkey}`;

      if (collection === "app.bsky.graph.listitem") {
        const record = body?.record || {};
        this.currentUserListItems.push({
          uri,
          listUri: record.list,
          subjectDid: record.subject,
        });
      }

      if (collection === "app.bsky.feed.post") {
        const record = body?.record;
        let embed;
        let quotedPostUri;
        const recordEmbed = record?.embed;

        if (recordEmbed?.$type === "app.bsky.embed.images") {
          embed = {
            $type: "app.bsky.embed.images#view",
            images: recordEmbed.images.map((img) => ({
              thumb: "",
              fullsize: "",
              alt: img.alt || "",
              aspectRatio: img.aspectRatio,
            })),
          };
        } else if (recordEmbed?.$type === "app.bsky.embed.video") {
          embed = {
            $type: "app.bsky.embed.video#view",
            cid: recordEmbed.video.ref.$link,
            playlist: "",
            alt: recordEmbed.alt || "",
            aspectRatio: recordEmbed.aspectRatio,
          };
        } else if (recordEmbed?.$type === "app.bsky.embed.external") {
          embed = {
            $type: "app.bsky.embed.external#view",
            external: {
              uri: recordEmbed.external.uri,
              title: recordEmbed.external.title || "",
              description: recordEmbed.external.description || "",
            },
          };
        } else if (recordEmbed?.$type === "app.bsky.embed.record") {
          const quotedRecordUri = recordEmbed.record.uri;
          const allQuotePosts = [
            ...this.timelinePosts,
            ...this.bookmarks,
            ...this.searchPosts,
            ...this.posts,
          ];
          const quotedPost = allQuotePosts.find(
            (p) => p.uri === quotedRecordUri,
          );
          const quotedGenerator = this.feedGenerators.find(
            (g) => g.uri === quotedRecordUri,
          );
          const quotedList = this.lists.find((l) => l.uri === quotedRecordUri);
          const quotedStarterPack = this.starterPacks.find(
            (s) => s.uri === quotedRecordUri,
          );
          if (quotedPost) {
            quotedPostUri = quotedRecordUri;
            embed = {
              $type: "app.bsky.embed.record#view",
              record: {
                $type: "app.bsky.embed.record#viewRecord",
                uri: quotedPost.uri,
                cid: quotedPost.cid,
                author: quotedPost.author,
                value: quotedPost.record,
                indexedAt: quotedPost.indexedAt,
                labels: [],
                embeds: [],
              },
            };
            quotedPost.quoteCount = (quotedPost.quoteCount || 0) + 1;
          } else if (quotedGenerator) {
            embed = {
              $type: "app.bsky.embed.record#view",
              record: {
                $type: "app.bsky.feed.defs#generatorView",
                ...quotedGenerator,
              },
            };
          } else if (quotedList) {
            embed = {
              $type: "app.bsky.embed.record#view",
              record: {
                $type: "app.bsky.graph.defs#listView",
                ...quotedList,
              },
            };
          } else if (quotedStarterPack) {
            embed = {
              $type: "app.bsky.embed.record#view",
              record: {
                $type: "app.bsky.graph.defs#starterPackViewBasic",
                ...quotedStarterPack,
              },
            };
          }
        }

        const post = createPost({
          uri,
          text: record?.text || "",
          authorHandle: userProfile.handle,
          authorDisplayName: userProfile.displayName,
          embed,
        });
        this.posts.push(post);

        if (quotedPostUri) {
          const existingQuotes = this.postQuotes.get(quotedPostUri) || [];
          existingQuotes.push(post);
          this.postQuotes.set(quotedPostUri, existingQuotes);
        }

        const isReply = !!record?.reply;

        if (isReply) {
          const parentUri = record.reply.parent.uri;
          const allReplyPosts = [
            ...this.timelinePosts,
            ...this.bookmarks,
            ...this.searchPosts,
            ...this.posts,
          ];
          const parentPost = allReplyPosts.find((p) => p.uri === parentUri);
          if (parentPost) {
            parentPost.replyCount = (parentPost.replyCount || 0) + 1;
          }
          const thread = this.postThreads.get(parentUri);
          if (thread) {
            thread.replies = thread.replies || [];
            thread.replies.push({
              $type: "app.bsky.feed.defs#threadViewPost",
              post,
              replies: [],
            });
          }
        }

        const feedKey = `${userProfile.did}-posts_and_author_threads`;
        const existing = this.authorFeeds.get(feedKey) || [];
        this.authorFeeds.set(feedKey, [post, ...existing]);
        if (!isReply) {
          const noRepliesKey = `${userProfile.did}-posts_no_replies`;
          const existingNoReplies = this.authorFeeds.get(noRepliesKey) || [];
          this.authorFeeds.set(noRepliesKey, [post, ...existingNoReplies]);
        }
      }

      if (collection === "app.bsky.feed.like") {
        const subjectUri = body?.record?.subject?.uri;
        const allPosts = [
          ...this.timelinePosts,
          ...this.bookmarks,
          ...this.searchPosts,
          ...this.posts,
        ];
        const post = allPosts.find((p) => p.uri === subjectUri);
        if (post) {
          post.viewer.like = uri;
          const feedKey = `${userProfile.did}-likes`;
          const existing = this.authorFeeds.get(feedKey) || [];
          this.authorFeeds.set(feedKey, [...existing, post]);
        }
      }

      if (collection === "app.bsky.feed.repost") {
        const subjectUri = body?.record?.subject?.uri;
        const allPosts = [
          ...this.timelinePosts,
          ...this.bookmarks,
          ...this.searchPosts,
          ...this.posts,
        ];
        const post = allPosts.find((p) => p.uri === subjectUri);
        if (post) {
          post.viewer.repost = uri;
          post.repostCount = (post.repostCount || 0) + 1;
          const feedKey = `${userProfile.did}-posts_and_author_threads`;
          const existing = this.authorFeeds.get(feedKey) || [];
          this.authorFeeds.set(feedKey, [post, ...existing]);
        }
      }

      if (collection === "app.bsky.graph.follow") {
        const subjectDid = body?.record?.subject;
        const profile = this.profiles.get(subjectDid);
        if (profile) {
          profile.viewer = { ...profile.viewer, following: uri };
          if (profile.followersCount !== undefined) {
            profile.followersCount++;
          }
          const follows = this.profileFollows.get(userProfile.did) || [];
          if (!follows.find((p) => p.did === subjectDid)) {
            follows.push(profile);
            this.profileFollows.set(userProfile.did, follows);
          }
        }
      }

      if (collection === "app.bsky.graph.block") {
        const subjectDid = body?.record?.subject;
        const profile = this.profiles.get(subjectDid);
        if (profile) {
          profile.viewer = { ...profile.viewer, blocking: uri };
        }
      }

      if (collection === "app.bsky.graph.listblock") {
        const subjectUri = body?.record?.subject;
        const list = this.lists.find((l) => l.uri === subjectUri);
        if (list) {
          list.viewer = { ...list.viewer, blocked: uri };
        }
      }

      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ uri, cid }),
      });
    });

    await page.route("**/xrpc/app.bsky.actor.putPreferences*", (route) => {
      const body = route.request().postDataJSON();
      const savedFeedsPref = body?.preferences?.find(
        (p) => p.$type === "app.bsky.actor.defs#savedFeedsPrefV2",
      );
      if (savedFeedsPref) {
        this.pinnedFeedUris = savedFeedsPref.items
          .filter((item) => item.type === "feed" && item.pinned)
          .map((item) => item.value);
        this.savedFeedUris = savedFeedsPref.items
          .filter((item) => item.type === "feed" && !item.pinned)
          .map((item) => item.value);
      }
      const hiddenPostsPref = body?.preferences?.find(
        (p) => p.$type === "app.bsky.actor.defs#improHiddenPostsPref",
      );
      if (hiddenPostsPref) {
        this.hiddenPostUris = hiddenPostsPref.items || [];
      }
      const labelersPref = body?.preferences?.find(
        (p) => p.$type === "app.bsky.actor.defs#labelersPref",
      );
      if (labelersPref) {
        this.labelerSubscriptions = labelersPref.labelers.map((l) => l.did);
      } else {
        this.labelerSubscriptions = [];
      }
      this.contentLabelPrefs = (body?.preferences || []).filter(
        (p) => p.$type === "app.bsky.actor.defs#contentLabelPref",
      );
      const mutedWordsPref = body?.preferences?.find(
        (p) => p.$type === "app.bsky.actor.defs#mutedWordsPref",
      );
      if (mutedWordsPref) {
        this.mutedWords = mutedWordsPref.items || [];
      }
      const installedPluginsPref = body?.preferences?.find(
        (p) => p.$type === "app.bsky.actor.defs#improInstalledPluginsPref",
      );
      if (installedPluginsPref) {
        this.installedPlugins = installedPluginsPref.plugins || [];
      }
      const pluginSettingsPrefs = (body?.preferences || []).filter(
        (p) => p.$type === "app.bsky.actor.defs#improPluginSettingsPref",
      );
      this.pluginSettings = new Map(
        pluginSettingsPrefs.map((p) => [p.pluginId, p.data]),
      );
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: "{}",
      });
    });

    await page.route(
      "**/xrpc/com.atproto.moderation.createReport*",
      (route) => {
        const body = route.request().postDataJSON();
        this.reportPayloads.push(body);
        return route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify({
            id: this.reportPayloads.length,
            reasonType: body?.reasonType,
            subject: body?.subject,
            reportedBy: userProfile.did,
            createdAt: new Date().toISOString(),
          }),
        });
      },
    );

    await page.route("**/xrpc/app.bsky.feed.sendInteractions*", (route) => {
      const body = route.request().postDataJSON();
      this.interactionPayloads.push(body);
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: "{}",
      });
    });

    await page.route("**/xrpc/com.atproto.repo.getRecord*", (route) => {
      const url = new URL(route.request().url());
      const collection = url.searchParams.get("collection");
      const rkey = url.searchParams.get("rkey");
      if (collection === "app.bsky.actor.profile" && rkey === "self") {
        const profile = this.profiles.get(userProfile.did) || userProfile;
        const value = {
          $type: "app.bsky.actor.profile",
          displayName: profile.displayName || "",
          description: profile.description || "",
        };
        if (profile.pinnedPost) {
          value.pinnedPost = profile.pinnedPost;
        }
        return route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify({
            uri: `at://${userProfile.did}/${collection}/${rkey}`,
            cid: "bafyreiprofilerecord",
            value,
          }),
        });
      }
      return route.fulfill({ status: 404, body: "{}" });
    });

    await page.route("**/xrpc/com.atproto.repo.putRecord*", (route) => {
      const body = route.request().postDataJSON();
      const collection = body?.collection;
      if (collection === "app.bsky.actor.profile") {
        const record = body?.record || {};
        const profile = this.profiles.get(userProfile.did) || {
          ...userProfile,
        };
        profile.displayName = record.displayName || "";
        profile.description = record.description || "";
        if (record.avatar) {
          profile.avatar = "mock-avatar-url";
        } else if (!record.avatar && record.avatar !== undefined) {
          profile.avatar = "";
        }
        if (record.banner) {
          profile.banner = "mock-banner-url";
        } else if (!record.banner && record.banner !== undefined) {
          profile.banner = "";
        }
        if (record.pinnedPost) {
          profile.pinnedPost = record.pinnedPost;
        } else {
          delete profile.pinnedPost;
        }
        this.profiles.set(userProfile.did, profile);
      }
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          uri: `at://${userProfile.did}/${collection}/self`,
          cid: "bafyreiupdatedrecord",
        }),
      });
    });

    await page.route("**/xrpc/com.atproto.repo.uploadBlob*", (route) =>
      route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          blob: {
            $type: "blob",
            ref: { $link: `bafkreimockblob${++this.blobCounter}` },
            mimeType: "image/jpeg",
            size: 50000,
          },
        }),
      }),
    );

    await page.route("**/xrpc/com.atproto.server.getServiceAuth*", (route) =>
      route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ token: "mock-service-auth-token" }),
      }),
    );

    await page.route("**/xrpc/app.bsky.video.getUploadLimits*", (route) =>
      route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          canUpload: this.videoCanUpload !== false,
          remainingDailyVideos: 25,
          remainingDailyBytes: 100_000_000,
          message: this.videoUploadMessage || "",
        }),
      }),
    );

    await page.route("**/xrpc/app.bsky.video.uploadVideo*", (route) => {
      this.videoJobCounter = (this.videoJobCounter || 0) + 1;
      const jobId = `mock-video-job-${this.videoJobCounter}`;
      this.videoJobPollCounts = this.videoJobPollCounts || new Map();
      this.videoJobPollCounts.set(jobId, 0);
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          jobId,
          did: userProfile.did,
          state: "JOB_STATE_ENCODING",
          progress: 0,
        }),
      });
    });

    await page.route("**/xrpc/app.bsky.video.getJobStatus*", (route) => {
      const url = new URL(route.request().url());
      const jobId = url.searchParams.get("jobId");
      if (this.videoJobShouldFail) {
        return route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify({
            jobStatus: {
              jobId,
              did: userProfile.did,
              state: "JOB_STATE_FAILED",
              progress: 0,
              error: "mock failure",
              message: "mock failure",
            },
          }),
        });
      }
      this.videoJobPollCounts = this.videoJobPollCounts || new Map();
      const count = (this.videoJobPollCounts.get(jobId) || 0) + 1;
      this.videoJobPollCounts.set(jobId, count);
      // Complete on the second poll
      if (count >= 2) {
        return route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify({
            jobStatus: {
              jobId,
              did: userProfile.did,
              state: "JOB_STATE_COMPLETED",
              progress: 1,
              blob: {
                $type: "blob",
                ref: { $link: `bafkreimockvideo${this.videoJobCounter}` },
                mimeType: "video/mp4",
                size: 1024,
              },
            },
          }),
        });
      }
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          jobStatus: {
            jobId,
            did: userProfile.did,
            state: "JOB_STATE_ENCODING",
            progress: 0.5,
          },
        }),
      });
    });

    await page.route(
      (url) => url.toString().includes("cardyb.bsky.app/v1/extract"),
      (route) => {
        const url = new URL(route.request().url());
        const targetUrl = url.searchParams.get("url");
        const meta = this.externalLinkCards.get(targetUrl) || {
          title: targetUrl,
          description: "",
        };
        return route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify(meta),
        });
      },
    );

    await page.route(
      "**/xrpc/app.bsky.actor.searchActorsTypeahead*",
      (route) => {
        return route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify({ actors: this.typeaheadProfiles }),
        });
      },
    );
  }
}

export class MockConstellation {
  constructor() {
    this.backlinks = new Map();
  }

  setBacklinks(subject, records) {
    this.backlinks.set(subject, records);
  }

  async setup(page) {
    await page.route("**/xrpc/blue.microcosm.links.getBacklinks*", (route) => {
      const url = new URL(route.request().url());
      const subject = url.searchParams.get("subject");
      const records = this.backlinks.get(subject) || [];
      return route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ records, cursor: "" }),
      });
    });
  }
}
