/// <cts-enable />
/**
 * Multi-user pattern test for scrabble.
 *
 * Unlike scrabble.test.tsx (one runtime, one identity), this runs ONE shared
 * game across two worker-isolated runtimes — exercising what a single runtime
 * cannot: each user draws into their OWN PerUser rack from the shared bag,
 * the shared player roster propagates across runtimes, and the "name already
 * taken" guard rejects a join from a genuinely different user.
 *
 * Joins go through the programmatic `joinWithName` stream (the headless seam
 * kept by the profile migration); the profile-wish UI path needs a browser.
 *
 * Cross-runtime read caveats baked into this test (reads of state another
 * runtime wrote, before this runtime's first own write):
 * - roster reads are INLINE literal accesses (players[0].name) in the
 *   assertion computed — `.map()`, loop-variable indexing, and module-level
 *   helper calls over the shared array do not resolve cross-runtime.
 * - a participant cannot read their own UNWRITTEN PerUser array (bob's rack
 *   before joining), so pre-join isolation is asserted via `myName` and via
 *   Alice's rack surviving Bob's join.
 *
 * Join order is deterministic (markers): Alice first, then Bob.
 */
import { action, computed, multiUserTest, pattern } from "commonfabric";
import Scrabble, { type GameOutput } from "./scrabble.tsx";

interface Setup {
  game: GameOutput;
}

export const setup = pattern(() => ({
  game: Scrabble({ gameName: "Multi-user Scrabble" }),
}));

export const alice = pattern<{ setup: Setup }>(({ setup }) => {
  const game = setup.game;

  const action_join = action(() => {
    game.joinWithName.send("Alice");
  });

  const assert_joined = computed(() =>
    game.myName === "Alice" &&
    (game.rack ?? []).length === 7 &&
    (game.players ?? []).length === 1 &&
    game.players?.[0]?.name === "Alice"
  );
  // Bob joining must not clobber Alice's PerUser identity or rack, and the
  // shared roster must resolve BOTH players in Alice's runtime.
  const assert_sees_both = computed(() =>
    (game.players ?? []).length === 2 &&
    game.players?.[0]?.name === "Alice" &&
    game.players?.[1]?.name === "Bob" &&
    game.myName === "Alice" &&
    (game.rack ?? []).length === 7
  );
  // Two players drew 7 tiles each from the one shared bag.
  const assert_bag_consumed = computed(() => game.bagIndex === 14);

  return {
    tests: [
      { action: action_join },
      { assertion: assert_joined },
      { label: "alice-joined" },
      { await: "bob-joined" },
      { assertion: assert_sees_both },
      { assertion: assert_bag_consumed },
    ],
  };
});

export const bob = pattern<{ setup: Setup }>(({ setup }) => {
  const game = setup.game;

  const action_join_as_alice = action(() => {
    game.joinWithName.send("Alice");
  });
  const action_join = action(() => {
    game.joinWithName.send("Bob");
  });

  // Shared roster propagated from Alice's runtime.
  const assert_sees_alice = computed(() =>
    (game.players ?? []).length === 1 &&
    game.players?.[0]?.name === "Alice"
  );
  // PerUser isolation: Alice's join must not leak into Bob's identity.
  const assert_not_joined_yet = computed(() => game.myName === "");
  // A DIFFERENT user trying to take Alice's name is rejected with a message
  // (PerSession) and stays unjoined.
  const assert_name_taken = computed(() =>
    game.message === "Name Alice is already taken." &&
    game.myName === ""
  );
  const assert_joined = computed(() =>
    game.myName === "Bob" &&
    (game.rack ?? []).length === 7 &&
    (game.players ?? []).length === 2 &&
    game.players?.[0]?.name === "Alice" &&
    game.players?.[1]?.name === "Bob"
  );

  return {
    tests: [
      { await: "alice-joined" },
      { assertion: assert_sees_alice },
      { assertion: assert_not_joined_yet },
      { action: action_join_as_alice },
      { assertion: assert_name_taken },
      { action: action_join },
      { assertion: assert_joined },
      { label: "bob-joined" },
    ],
  };
});

export default multiUserTest({ setup, participants: { alice, bob } });
