import { test, expect } from "../../base.js";
import { login } from "../../helpers.js";
import { MockServer } from "../../mockServer.js";
import {
  createPost,
  createConvo,
  createProfile,
} from "../../../shared/factories.js";

const postUri = "at://did:plc:author1/app.bsky.feed.post/joinLink1";
const postPath = "/profile/author1.bsky.social/post/joinLink1";

function makeJoinLinkPreview(overrides = {}) {
  return {
    $type: "chat.bsky.group.defs#joinLinkPreviewView",
    code: "abcd1234",
    name: "Friends of Bsky",
    memberCount: 5,
    memberLimit: 50,
    joinRule: "open",
    requireApproval: false,
    owner: {
      did: "did:plc:owner",
      handle: "owner.bsky.social",
      displayName: "Owner",
      avatar: "",
      viewer: {},
      labels: [],
      createdAt: "2025-01-01T00:00:00.000Z",
    },
    viewer: {},
    ...overrides,
  };
}

function buildInvitePost(preview) {
  return createPost({
    uri: postUri,
    text: "Join us!",
    authorHandle: "author1.bsky.social",
    authorDisplayName: "Author One",
    embed: {
      $type: "chat.bsky.embed.joinLink#view",
      joinLinkPreview: preview,
    },
  });
}

async function setupInvitePostThread(page, preview, { mockServer } = {}) {
  const server = mockServer ?? new MockServer();
  const post = buildInvitePost(preview);
  server.addPosts([post]);
  server.setPostThread(post.uri, {
    $type: "app.bsky.feed.defs#threadViewPost",
    post,
    replies: [],
  });
  await server.setup(page);
  return server;
}

test.describe("Join link embed flows", () => {
  test.describe("render states", () => {
    test("renders open action when viewer is a member of a different chat", async ({
      page,
    }) => {
      await setupInvitePostThread(
        page,
        makeJoinLinkPreview({ convo: { id: "other-convo" } }),
      );
      await login(page);
      await page.goto(postPath);

      await expect(
        page.locator('[data-testid="join-link-embed-action"]'),
      ).toHaveAttribute("data-teststate", "open", { timeout: 10000 });
    });

    test("renders follow-required action when joinRule is followedByOwner and viewer doesn't follow", async ({
      page,
    }) => {
      await setupInvitePostThread(
        page,
        makeJoinLinkPreview({ joinRule: "followedByOwner" }),
      );
      await login(page);
      await page.goto(postPath);

      const action = page.locator('[data-testid="join-link-embed-action"]');
      await expect(action).toHaveAttribute(
        "data-teststate",
        "follow-required",
        {
          timeout: 10000,
        },
      );
      await expect(action).toBeDisabled();
    });

    test("renders requested action when viewer has already requested", async ({
      page,
    }) => {
      await setupInvitePostThread(
        page,
        makeJoinLinkPreview({
          viewer: { requestedAt: "2026-06-01T00:00:00Z" },
        }),
      );
      await login(page);
      await page.goto(postPath);

      await expect(
        page.locator('[data-testid="join-link-embed-action"]'),
      ).toHaveAttribute("data-teststate", "requested", { timeout: 10000 });
    });
  });

  test.describe("click behaviors", () => {
    test("clicking copy writes the invite URL to the clipboard and shows a toast", async ({
      page,
      browserName,
    }) => {
      // The copy action only shows when the invite points at the current chat,
      // which only happens inside chatDetail. Render an invite embed in a DM
      // where the convo id matches.
      const mockServer = new MockServer();
      const alice = createProfile({
        did: "did:plc:alice1",
        handle: "alice.bsky.social",
        displayName: "Alice",
      });
      const convo = createConvo({ id: "convo-1", otherMember: alice });
      mockServer.addConvos([convo]);
      mockServer.addConvoMessages("convo-1", [
        {
          $type: "chat.bsky.convo.defs#messageView",
          id: "msg-1",
          rev: "rev-msg-1",
          text: "",
          facets: [],
          sender: { did: alice.did },
          sentAt: "2025-01-15T12:00:00.000Z",
          reactions: [],
          embed: {
            $type: "chat.bsky.embed.joinLink#view",
            joinLinkPreview: makeJoinLinkPreview({
              convo: { id: "convo-1" },
            }),
          },
        },
      ]);
      await mockServer.setup(page);

      if (browserName === "chromium") {
        await page
          .context()
          .grantPermissions(["clipboard-read", "clipboard-write"]);
      }

      await login(page);
      await page.goto("/messages/convo-1");

      await expect(
        page.locator('[data-testid="join-link-embed-action"]'),
      ).toHaveAttribute("data-teststate", "copy", { timeout: 10000 });
      await page.locator('[data-testid="join-link-embed-action"]').click();

      await expect(page.locator('[data-testid="toast"]')).toContainText(
        "Copied to clipboard",
      );

      if (browserName === "chromium") {
        const clipboardText = await page.evaluate(() =>
          navigator.clipboard.readText(),
        );
        expect(clipboardText).toBe("https://bsky.app/chat/abcd1234");
      }
    });

    test("clicking open navigates to the chat", async ({ page }) => {
      const otherProfile = createProfile({
        did: "did:plc:other1",
        handle: "other.bsky.social",
        displayName: "Other",
      });
      const otherConvo = createConvo({
        id: "other-convo",
        otherMember: otherProfile,
      });
      const mockServer = new MockServer();
      mockServer.addConvos([otherConvo]);
      mockServer.addConvoMessages("other-convo", []);
      await setupInvitePostThread(
        page,
        makeJoinLinkPreview({ convo: { id: "other-convo" } }),
        { mockServer },
      );
      await login(page);
      await page.goto(postPath);

      await expect(
        page.locator('[data-testid="join-link-embed-action"]'),
      ).toHaveAttribute("data-teststate", "open", { timeout: 10000 });
      await page.locator('[data-testid="join-link-embed-action"]').click();

      await expect(page).toHaveURL(/\/messages\/other-convo$/, {
        timeout: 10000,
      });
      await expect(page.locator("#chat-detail-view")).toBeVisible();
    });

    test("clicking request opens the dialog and confirming shows a success toast", async ({
      page,
    }) => {
      await setupInvitePostThread(
        page,
        makeJoinLinkPreview({ requireApproval: true }),
      );
      await login(page);
      await page.goto(postPath);

      await expect(
        page.locator('[data-testid="join-link-embed-action"]'),
      ).toHaveAttribute("data-teststate", "request", { timeout: 10000 });
      await page.locator('[data-testid="join-link-embed-action"]').click();

      await expect(
        page.locator('[data-testid="modal-confirm-button"]'),
      ).toBeVisible();
      await page.locator('[data-testid="modal-confirm-button"]').click();

      await expect(page.locator('[data-testid="toast"]')).toContainText(
        "Request sent",
      );
      await expect(
        page.locator('[data-testid="modal-confirm-button"]'),
      ).toHaveCount(0);
    });

    test("clicking requested shows a 'request pending' toast", async ({
      page,
    }) => {
      await setupInvitePostThread(
        page,
        makeJoinLinkPreview({
          viewer: { requestedAt: "2026-06-01T00:00:00Z" },
        }),
      );
      await login(page);
      await page.goto(postPath);

      const action = page.locator('[data-testid="join-link-embed-action"]');
      await expect(action).toHaveAttribute("data-teststate", "requested", {
        timeout: 10000,
      });
      await action.click();

      await expect(page.locator('[data-testid="modal-title"]')).toContainText(
        "Request pending",
      );
    });
  });

  test.describe("join confirmation", () => {
    test("on success, the action flips to the requested state", async ({
      page,
    }) => {
      await setupInvitePostThread(
        page,
        makeJoinLinkPreview({ requireApproval: true }),
      );
      await login(page);
      await page.goto(postPath);

      const action = page.locator('[data-testid="join-link-embed-action"]');
      await expect(action).toHaveAttribute("data-teststate", "request", {
        timeout: 10000,
      });
      await action.click();

      await page.locator('[data-testid="modal-confirm-button"]').click();

      await expect(action).toHaveAttribute("data-teststate", "requested", {
        timeout: 10000,
      });
    });

    test("on open-join success, navigates to the joined chat", async ({
      page,
    }) => {
      const owner = createProfile({
        did: "did:plc:owner",
        handle: "owner.bsky.social",
        displayName: "Owner",
      });
      const joinedConvo = createConvo({
        id: "joined-convo",
        otherMember: owner,
      });
      const mockServer = new MockServer();
      mockServer.addConvos([joinedConvo]);
      mockServer.addConvoMessages("joined-convo", []);
      mockServer.setJoinLinkJoinedConvo("abcd1234", joinedConvo);
      await setupInvitePostThread(page, makeJoinLinkPreview(), { mockServer });
      await login(page);
      await page.goto(postPath);

      const action = page.locator('[data-testid="join-link-embed-action"]');
      await expect(action).toHaveAttribute("data-teststate", "join", {
        timeout: 10000,
      });
      await action.click();
      await page.locator('[data-testid="modal-confirm-button"]').click();

      await expect(page).toHaveURL(/\/messages\/joined-convo$/, {
        timeout: 10000,
      });
      await expect(page.locator("#chat-detail-view")).toBeVisible();
    });

    test("on failure, the dialog stays open and an error toast appears", async ({
      page,
    }) => {
      const mockServer = new MockServer();
      mockServer.failJoinLinkRequest("abcd1234");
      await setupInvitePostThread(page, makeJoinLinkPreview(), { mockServer });
      await login(page);
      await page.goto(postPath);

      const action = page.locator('[data-testid="join-link-embed-action"]');
      await expect(action).toHaveAttribute("data-teststate", "join", {
        timeout: 10000,
      });
      await action.click();

      await page.locator('[data-testid="modal-confirm-button"]').click();

      await expect(page.locator('[data-testid="toast"]')).toContainText(
        "Could not send join request",
      );
      await expect(
        page.locator('[data-testid="modal-confirm-button"]'),
      ).toBeVisible();
    });

    test("cancel button closes the dialog without sending the request", async ({
      page,
    }) => {
      await setupInvitePostThread(page, makeJoinLinkPreview());
      await login(page);
      await page.goto(postPath);

      await page.locator('[data-testid="join-link-embed-action"]').click();
      await expect(
        page.locator('[data-testid="modal-confirm-button"]'),
      ).toBeVisible();

      await page.locator('[data-testid="modal-cancel-button"]').click();

      await expect(
        page.locator('[data-testid="modal-confirm-button"]'),
      ).toHaveCount(0);
      await expect(
        page.locator('[data-testid="join-link-embed-action"]'),
      ).toHaveAttribute("data-teststate", "join");
    });

    test("clicking the backdrop closes the dialog", async ({ page }) => {
      await setupInvitePostThread(page, makeJoinLinkPreview());
      await login(page);
      await page.goto(postPath);

      await page.locator('[data-testid="join-link-embed-action"]').click();
      const dialog = page.locator('[data-testid="join-chat-modal"]');
      await expect(dialog).toBeVisible();

      // Click at the very top-left corner (outside the dialog content)
      await dialog.click({ position: { x: 2, y: 2 } });

      await expect(dialog).toHaveCount(0);
    });
  });
});
