/**
 * Firestore Security Rules — QA Matrix
 *
 * Covers every client-read/write path in firestore.rules.
 * Run against the local emulator:
 *
 *   firebase emulators:start --only firestore
 *   cd firestore-tests && npm test
 *
 * Each test name follows the pattern: "[path] [operation] [who] → [allowed|denied]"
 * "allowed" = assertSucceeds, "denied" = assertFails.
 */

import {
  assertFails,
  assertSucceeds,
  initializeTestEnvironment,
  RulesTestContext,
  RulesTestEnvironment,
} from "@firebase/rules-unit-testing";
import {
  doc,
  setDoc,
  getDoc,
  updateDoc,
  deleteDoc,
  collection,
  addDoc,
  Timestamp,
  serverTimestamp,
} from "firebase/firestore";

// ── Test environment ──────────────────────────────────────────────────────────

const PROJECT_ID = process.env.GCLOUD_PROJECT ?? "couples-connect-dev";
const FIRESTORE_PORT = Number(process.env.FIRESTORE_EMULATOR_PORT ?? "8180");
let testEnv: RulesTestEnvironment;
let aliceContext: RulesTestContext;
let bobContext: RulesTestContext;
let charlieContext: RulesTestContext;
let anonContext: RulesTestContext;

beforeAll(async () => {
  testEnv = await initializeTestEnvironment({
    projectId: PROJECT_ID,
    firestore: {
      host: "127.0.0.1",
      port: FIRESTORE_PORT,
    },
  });
  aliceContext = testEnv.authenticatedContext(UID_A);
  bobContext = testEnv.authenticatedContext(UID_B);
  charlieContext = testEnv.authenticatedContext(UID_C);
  anonContext = testEnv.unauthenticatedContext();
});

afterAll(async () => {
  await testEnv?.cleanup();
});

afterEach(async () => {
  await testEnv.clearFirestore();
});

// ── Helpers ───────────────────────────────────────────────────────────────────

const UID_A = "user_alice";
const UID_B = "user_bob";
const UID_C = "user_charlie"; // outsider
const COUPLE_ID = "couple_ab";
const COUPLE_DOC = {
  id: COUPLE_ID,
  userIds: [UID_A, UID_B],
  inviteCode: "ABC123",
  createdAt: 1_000_000,
  streakCount: 0,
  lastAnsweredAt: null,
  encryptionVersion: 2,
  wrappedCoupleKey: "wrapped-key",
  kdfSalt: "salt",
  kdfParams: "argon2id",
};
const CIPHERTEXT = "enc:v1:YWJj";

/** Seed documents that rules' helper functions need (e.g. isCouplesMember reads the couple). */
async function seedCouple() {
  await testEnv.withSecurityRulesDisabled(async (ctx) => {
    await setDoc(doc(ctx.firestore(), `couples/${COUPLE_ID}`), COUPLE_DOC);
  });
}

async function seedUser(uid: string, coupleId?: string) {
  await testEnv.withSecurityRulesDisabled(async (ctx) => {
    await setDoc(doc(ctx.firestore(), `users/${uid}`), {
      displayName: uid,
      coupleId: coupleId ?? null,
    });
  });
}

const alice = () => aliceContext;
const bob = () => bobContext;
const charlie = () => charlieContext;
const anon = () => anonContext;

// ── users/{uid} ───────────────────────────────────────────────────────────────

describe("users/{uid}", () => {
  test("owner can read own doc — allowed", async () => {
    await seedUser(UID_A);
    await assertSucceeds(getDoc(doc(alice().firestore(), `users/${UID_A}`)));
  });

  test("other user cannot read alice's doc — denied", async () => {
    await seedUser(UID_A);
    await assertFails(getDoc(doc(bob().firestore(), `users/${UID_A}`)));
  });

  test("unauthenticated cannot read — denied", async () => {
    await seedUser(UID_A);
    await assertFails(getDoc(doc(anon().firestore(), `users/${UID_A}`)));
  });

  test("owner can create own doc without hasPremium — allowed", async () => {
    await assertSucceeds(
      setDoc(doc(alice().firestore(), `users/${UID_A}`), { displayName: "Alice" })
    );
  });

  test("owner cannot set hasPremium on create — denied", async () => {
    await assertFails(
      setDoc(doc(alice().firestore(), `users/${UID_A}`), {
        displayName: "Alice",
        hasPremium: true,
      })
    );
  });

  test("owner can update own doc (non-premium fields) — allowed", async () => {
    await seedUser(UID_A);
    await assertSucceeds(
      updateDoc(doc(alice().firestore(), `users/${UID_A}`), { displayName: "Alice 2" })
    );
  });

  test("owner cannot update hasPremium — denied", async () => {
    await seedUser(UID_A);
    await assertFails(
      updateDoc(doc(alice().firestore(), `users/${UID_A}`), { hasPremium: true })
    );
  });
});

// ── users/{uid}/entitlements/{doc} ───────────────────────────────────────────

describe("users/{uid}/entitlements/{doc}", () => {
  test("owner can read entitlements — allowed", async () => {
    await testEnv.withSecurityRulesDisabled(async (ctx) => {
      await setDoc(
        doc(ctx.firestore(), `users/${UID_A}/entitlements/premium`),
        { active: true }
      );
    });
    await assertSucceeds(
      getDoc(doc(alice().firestore(), `users/${UID_A}/entitlements/premium`))
    );
  });

  test("owner cannot write entitlements (server-only) — denied", async () => {
    await assertFails(
      setDoc(doc(alice().firestore(), `users/${UID_A}/entitlements/premium`), {
        active: true,
      })
    );
  });

  test("other user cannot read entitlements — denied", async () => {
    await assertFails(
      getDoc(doc(bob().firestore(), `users/${UID_A}/entitlements/premium`))
    );
  });
});

// ── users/{uid}/notification_queue/{id} ──────────────────────────────────────

describe("users/{uid}/notification_queue/{id}", () => {
  test("no client read — denied", async () => {
    await assertFails(
      getDoc(
        doc(alice().firestore(), `users/${UID_A}/notification_queue/notif1`)
      )
    );
  });

  test("no client write — denied", async () => {
    await assertFails(
      setDoc(
        doc(alice().firestore(), `users/${UID_A}/notification_queue/notif1`),
        { type: "partner_answered" }
      )
    );
  });
});

// ── date_ideas/{id} ──────────────────────────────────────────────────────────

describe("date_ideas/{id}", () => {
  test("any auth user can read — allowed", async () => {
    await testEnv.withSecurityRulesDisabled(async (ctx) => {
      await setDoc(doc(ctx.firestore(), "date_ideas/idea1"), { title: "Picnic" });
    });
    await assertSucceeds(getDoc(doc(alice().firestore(), "date_ideas/idea1")));
  });

  test("client cannot write date_ideas — denied", async () => {
    await assertFails(
      setDoc(doc(alice().firestore(), "date_ideas/idea1"), { title: "Picnic" })
    );
  });

  test("unauthenticated cannot read — denied", async () => {
    await assertFails(getDoc(doc(anon().firestore(), "date_ideas/idea1")));
  });
});

// ── invites/{code} ───────────────────────────────────────────────────────────

describe("invites/{code}", () => {
  const INVITE_CODE = "ABC123";
  const expiresAt = Timestamp.fromMillis(Date.now() + 3_600_000);

  async function seedInvite(extra?: Record<string, unknown>) {
    await testEnv.withSecurityRulesDisabled(async (ctx) => {
      await setDoc(doc(ctx.firestore(), `invites/${INVITE_CODE}`), {
        inviterUserId: UID_A,
        code: INVITE_CODE,
        status: "pending",
        createdAt: Timestamp.now(),
        expiresAt,
        wrappedCoupleKey: "wrapped-key",
        kdfSalt: "salt",
        kdfParams: "argon2id",
        ...extra,
      });
    });
  }

  test("inviter can create valid invite — allowed", async () => {
    await assertSucceeds(
      setDoc(doc(alice().firestore(), `invites/${INVITE_CODE}`), {
        inviterUserId: UID_A,
        code: INVITE_CODE,
        status: "pending",
        createdAt: Timestamp.now(),
        expiresAt,
        wrappedCoupleKey: "wrapped-key",
        kdfSalt: "salt",
        kdfParams: "argon2id",
      })
    );
  });

  test("inviter cannot set coupleId on create — denied", async () => {
    await assertFails(
      setDoc(doc(alice().firestore(), `invites/${INVITE_CODE}`), {
        inviterUserId: UID_A,
        code: INVITE_CODE,
        status: "pending",
        createdAt: Timestamp.now(),
        expiresAt,
        wrappedCoupleKey: "wrapped-key",
        kdfSalt: "salt",
        kdfParams: "argon2id",
        coupleId: "injected",
      })
    );
  });

  test("creator cannot set inviterUserId to another user — denied", async () => {
    await assertFails(
      setDoc(doc(alice().firestore(), `invites/${INVITE_CODE}`), {
        inviterUserId: UID_B,
        code: INVITE_CODE,
        status: "pending",
        createdAt: Timestamp.now(),
        expiresAt,
        wrappedCoupleKey: "wrapped-key",
        kdfSalt: "salt",
        kdfParams: "argon2id",
      })
    );
  });

  test("inviter can read own invite — allowed", async () => {
    await seedInvite();
    await assertSucceeds(getDoc(doc(alice().firestore(), `invites/${INVITE_CODE}`)));
  });

  test("unpaired user cannot read pending invite to prevent enumeration — denied", async () => {
    await seedInvite();
    await seedUser(UID_B); // user doc exists, no coupleId
    await assertFails(getDoc(doc(bob().firestore(), `invites/${INVITE_CODE}`)));
  });

  test("already-paired outsider cannot read invite — denied", async () => {
    await seedInvite();
    await seedUser(UID_C, "another-couple");
    await assertFails(getDoc(doc(charlie().firestore(), `invites/${INVITE_CODE}`)));
  });

  test("acceptor cannot directly update invite (server-only) — denied", async () => {
    await seedInvite();
    await seedUser(UID_B);
    await assertFails(
      updateDoc(doc(bob().firestore(), `invites/${INVITE_CODE}`), {
        status: "accepted",
        acceptedByUserId: UID_B,
        acceptedAt: Timestamp.now(),
      })
    );
  });

  test("inviter cannot accept own invite — denied", async () => {
    await seedInvite();
    await seedUser(UID_A);
    await assertFails(
      updateDoc(doc(alice().firestore(), `invites/${INVITE_CODE}`), {
        status: "accepted",
        acceptedByUserId: UID_A,
        acceptedAt: Timestamp.now(),
      })
    );
  });
});

// ── couples/{coupleId} ───────────────────────────────────────────────────────

describe("couples/{coupleId}", () => {
  beforeEach(seedCouple);

  test("member can read couple — allowed", async () => {
    await assertSucceeds(getDoc(doc(alice().firestore(), `couples/${COUPLE_ID}`)));
  });

  test("outsider cannot read couple — denied", async () => {
    await assertFails(getDoc(doc(charlie().firestore(), `couples/${COUPLE_ID}`)));
  });

  test("no client can create couple (server-only) — denied", async () => {
    await assertFails(
      setDoc(doc(alice().firestore(), "couples/newCouple"), {
        userIds: [UID_A, UID_C],
        inviteCode: "XYZ789",
        createdAt: 1,
        streakCount: 0,
      })
    );
  });

  test("member cannot inject a custom couple field — denied", async () => {
    await assertFails(
      updateDoc(doc(alice().firestore(), `couples/${COUPLE_ID}`), {
        someCustomField: "hello",
      })
    );
  });

  test("member can update streakCount from client — allowed", async () => {
    await assertSucceeds(
      updateDoc(doc(alice().firestore(), `couples/${COUPLE_ID}`), {
        streakCount: 5,
        lastAnsweredAt: Date.now(),
      })
    );
  });

  test("member cannot change userIds — denied", async () => {
    await assertFails(
      updateDoc(doc(alice().firestore(), `couples/${COUPLE_ID}`), {
        userIds: [UID_A, UID_C],
      })
    );
  });

  test("member cannot change inviteCode — denied", async () => {
    await assertFails(
      updateDoc(doc(alice().firestore(), `couples/${COUPLE_ID}`), {
        inviteCode: "HACKED1",
      })
    );
  });

  test("member cannot change createdAt — denied", async () => {
    await assertFails(
      updateDoc(doc(alice().firestore(), `couples/${COUPLE_ID}`), {
        createdAt: 0,
      })
    );
  });

  test("couple cannot be deleted by client — denied", async () => {
    await assertFails(deleteDoc(doc(alice().firestore(), `couples/${COUPLE_ID}`)));
  });

  describe("encryption migration", () => {
    const legacyCouple = {
      id: COUPLE_ID,
      userIds: [UID_A, UID_B],
      inviteCode: "ABC123",
      createdAt: 1_000_000,
      streakCount: 0,
      lastAnsweredAt: null,
      encryptionVersion: 0,
    };

    test("a member can start encryption migration — allowed", async () => {
      await testEnv.withSecurityRulesDisabled(async (ctx) => {
        await setDoc(doc(ctx.firestore(), `couples/${COUPLE_ID}`), legacyCouple);
      });

      await assertSucceeds(updateDoc(doc(alice().firestore(), `couples/${COUPLE_ID}`), {
        wrappedCoupleKey: "wrapped-key",
        kdfSalt: "salt",
        kdfParams: "argon2id",
        encryptionVersion: 1,
        encryptionMigrationUsers: {},
      }));
    });

    test("a member can mark only their own migration complete — allowed", async () => {
      await testEnv.withSecurityRulesDisabled(async (ctx) => {
        const versionOne = { ...COUPLE_DOC, encryptionVersion: 1 };
        delete (versionOne as Record<string, unknown>).encryptionMigrationUsers;
        await setDoc(doc(ctx.firestore(), `couples/${COUPLE_ID}`), versionOne);
      });

      await assertSucceeds(updateDoc(doc(alice().firestore(), `couples/${COUPLE_ID}`), {
        encryptionVersion: 1,
        encryptionMigrationUsers: { [UID_A]: true },
      }));
    });

    test("a member cannot claim their partner migrated — denied", async () => {
      await testEnv.withSecurityRulesDisabled(async (ctx) => {
        await setDoc(doc(ctx.firestore(), `couples/${COUPLE_ID}`), {
          ...COUPLE_DOC,
          encryptionVersion: 1,
          encryptionMigrationUsers: {},
        });
      });

      await assertFails(updateDoc(doc(alice().firestore(), `couples/${COUPLE_ID}`), {
        encryptionVersion: 1,
        encryptionMigrationUsers: { [UID_B]: true },
      }));
    });

    test("version 2 requires both partners to complete migration — denied", async () => {
      await testEnv.withSecurityRulesDisabled(async (ctx) => {
        await setDoc(doc(ctx.firestore(), `couples/${COUPLE_ID}`), {
          ...COUPLE_DOC,
          encryptionVersion: 1,
          encryptionMigrationUsers: {},
        });
      });

      await assertFails(updateDoc(doc(alice().firestore(), `couples/${COUPLE_ID}`), {
        encryptionVersion: 2,
        encryptionMigrationUsers: { [UID_A]: true },
      }));
    });

    test("the second partner can complete migration and promote to version 2 — allowed", async () => {
      await testEnv.withSecurityRulesDisabled(async (ctx) => {
        await setDoc(doc(ctx.firestore(), `couples/${COUPLE_ID}`), {
          ...COUPLE_DOC,
          encryptionVersion: 1,
          encryptionMigrationUsers: { [UID_A]: true },
        });
      });

      await assertSucceeds(updateDoc(doc(bob().firestore(), `couples/${COUPLE_ID}`), {
        encryptionVersion: 2,
        encryptionMigrationUsers: { [UID_A]: true, [UID_B]: true },
      }));
    });
  });
});

// ── couples/{coupleId}/sessions/{sessionId} ──────────────────────────────────

describe("couples/{coupleId}/sessions/{sessionId}", () => {
  const SESSION_PATH = `couples/${COUPLE_ID}/sessions/sess1`;

  beforeEach(seedCouple);

  test("member can read session — allowed", async () => {
    await testEnv.withSecurityRulesDisabled(async (ctx) => {
      await setDoc(doc(ctx.firestore(), SESSION_PATH), {
        startedByUserId: UID_A,
        status: "active",
      });
    });
    await assertSucceeds(getDoc(doc(alice().firestore(), SESSION_PATH)));
  });

  test("outsider cannot read session — denied", async () => {
    await testEnv.withSecurityRulesDisabled(async (ctx) => {
      await setDoc(doc(ctx.firestore(), SESSION_PATH), {
        startedByUserId: UID_A,
        status: "active",
      });
    });
    await assertFails(getDoc(doc(charlie().firestore(), SESSION_PATH)));
  });

  test("member can create session with own startedByUserId — allowed", async () => {
    await assertSucceeds(
      setDoc(doc(alice().firestore(), SESSION_PATH), {
        startedByUserId: UID_A,
        status: "active",
      })
    );
  });

  test("member cannot create session with other's startedByUserId — denied", async () => {
    await assertFails(
      setDoc(doc(alice().firestore(), SESSION_PATH), {
        startedByUserId: UID_B,
        status: "active",
      })
    );
  });

  test("starter can update status to completed — allowed", async () => {
    await testEnv.withSecurityRulesDisabled(async (ctx) => {
      await setDoc(doc(ctx.firestore(), SESSION_PATH), {
        startedByUserId: UID_A,
        status: "active",
      });
    });
    await assertSucceeds(
      updateDoc(doc(alice().firestore(), SESSION_PATH), {
        status: "completed",
        completedAt: Timestamp.now(),
      })
    );
  });

  test("non-starter can complete an active shared session — allowed", async () => {
    await testEnv.withSecurityRulesDisabled(async (ctx) => {
      await setDoc(doc(ctx.firestore(), SESSION_PATH), {
        startedByUserId: UID_A,
        status: "active",
      });
    });
    await assertSucceeds(
      updateDoc(doc(bob().firestore(), SESSION_PATH), {
        status: "completed",
        completedAt: Timestamp.now(),
      })
    );
  });

  test("client cannot delete session — denied", async () => {
    await testEnv.withSecurityRulesDisabled(async (ctx) => {
      await setDoc(doc(ctx.firestore(), SESSION_PATH), {
        startedByUserId: UID_A,
        status: "active",
      });
    });
    await assertFails(deleteDoc(doc(alice().firestore(), SESSION_PATH)));
  });
});

// ── couples/{coupleId}/question_threads/{threadId} ───────────────────────────

describe("couples/{coupleId}/question_threads/{threadId}", () => {
  const THREAD_PATH = `couples/${COUPLE_ID}/question_threads/thread1`;

  beforeEach(seedCouple);

  async function seedThread(extra?: Record<string, unknown>) {
    await setDoc(doc(alice().firestore(), THREAD_PATH), {
      questionId: "q1",
      categoryId: "emotional_intimacy",
      status: "NOT_STARTED",
      currentIndex: 0,
      createdByUserId: UID_A,
      createdAt: Timestamp.now(),
      updatedAt: Timestamp.now(),
      ...extra,
    });
  }

  test("member can create thread with own createdByUserId — allowed", async () => {
    await assertSucceeds(
      setDoc(doc(alice().firestore(), THREAD_PATH), {
        questionId: "q1",
        categoryId: "emotional_intimacy",
        status: "NOT_STARTED",
        currentIndex: 0,
        createdByUserId: UID_A,
        createdAt: serverTimestamp(),
        updatedAt: serverTimestamp(),
      })
    );
  });

  test("member cannot create thread with other's createdByUserId — denied", async () => {
    await assertFails(
      setDoc(doc(alice().firestore(), THREAD_PATH), {
        questionId: "q1",
        categoryId: "emotional_intimacy",
        status: "NOT_STARTED",
        currentIndex: 0,
        createdByUserId: UID_B,
        createdAt: serverTimestamp(),
        updatedAt: serverTimestamp(),
      })
    );
  });

  test("member cannot create thread without createdByUserId — denied", async () => {
    await assertFails(
      setDoc(doc(alice().firestore(), THREAD_PATH), {
        questionId: "q1",
        categoryId: "emotional_intimacy",
        status: "NOT_STARTED",
        currentIndex: 0,
        createdAt: serverTimestamp(),
        updatedAt: serverTimestamp(),
      })
    );
  });

  test("member can read thread — allowed", async () => {
    await seedThread();
    await assertSucceeds(getDoc(doc(bob().firestore(), THREAD_PATH)));
  });

  test("outsider cannot read thread — denied", async () => {
    await seedThread();
    await assertFails(getDoc(doc(charlie().firestore(), THREAD_PATH)));
  });

  test("member can update status (valid transition, no extra fields) — allowed", async () => {
    await seedThread({ status: "NOT_STARTED", currentIndex: 0 });
    await assertSucceeds(
      updateDoc(doc(alice().firestore(), THREAD_PATH), {
        status: "ANSWERED_BY_ONE",
      })
    );
  });

  test("status update with updatedAt field — denied", async () => {
    await seedThread({ status: "NOT_STARTED" });
    await assertFails(
      updateDoc(doc(alice().firestore(), THREAD_PATH), {
        status: "ANSWERED_BY_ONE",
        updatedAt: serverTimestamp(),
      })
    );
  });

  test("invalid status transition — denied", async () => {
    await seedThread({ status: "NOT_STARTED" });
    await assertFails(
      updateDoc(doc(alice().firestore(), THREAD_PATH), {
        status: "REVEALED",
      })
    );
  });

  test("currentIndex can be incremented — allowed", async () => {
    await seedThread({ status: "NOT_STARTED", currentIndex: 0 });
    await assertSucceeds(
      updateDoc(doc(alice().firestore(), THREAD_PATH), {
        currentIndex: 1,
      })
    );
  });

  test("currentIndex cannot be decremented — denied", async () => {
    await seedThread({ status: "ANSWERED_BY_ONE", currentIndex: 2 });
    await assertFails(
      updateDoc(doc(alice().firestore(), THREAD_PATH), {
        currentIndex: 1,
      })
    );
  });

  test("client cannot delete thread — denied", async () => {
    await seedThread();
    await assertFails(deleteDoc(doc(alice().firestore(), THREAD_PATH)));
  });

  // ── answers ────────────────────────────────────────────────────────────────

  describe("answers/{userId}", () => {
    const ANSWER_PATH = `${THREAD_PATH}/answers/${UID_A}`;

    test("owner can write own answer — allowed", async () => {
      await assertSucceeds(
        setDoc(doc(alice().firestore(), ANSWER_PATH), {
          userId: UID_A,
          questionId: "q1",
          answerType: "written",
          writtenText: CIPHERTEXT,
          createdAt: serverTimestamp(),
          updatedAt: serverTimestamp(),
        })
      );
    });

    test("owner cannot write a plaintext answer — denied", async () => {
      await assertFails(
        setDoc(doc(alice().firestore(), ANSWER_PATH), {
          userId: UID_A,
          questionId: "q1",
          answerType: "written",
          writtenText: "Hello",
          createdAt: serverTimestamp(),
          updatedAt: serverTimestamp(),
        })
      );
    });

    test("other user cannot write alice's answer — denied", async () => {
      await assertFails(
        setDoc(doc(bob().firestore(), ANSWER_PATH), {
          userId: UID_A,
          questionId: "q1",
          answerType: "written",
          writtenText: "Hacked",
        })
      );
    });

    test("an outsider cannot write under their own user id — denied", async () => {
      await assertFails(
        setDoc(doc(charlie().firestore(), `${THREAD_PATH}/answers/${UID_C}`), {
          userId: UID_C,
          questionId: "q1",
          answerType: "written",
          writtenText: CIPHERTEXT,
          createdAt: serverTimestamp(),
          updatedAt: serverTimestamp(),
        })
      );
    });

    test("couple member can read any answer — allowed", async () => {
      await testEnv.withSecurityRulesDisabled(async (ctx) => {
        await setDoc(doc(ctx.firestore(), ANSWER_PATH), { userId: UID_A });
      });
      await assertSucceeds(getDoc(doc(bob().firestore(), ANSWER_PATH)));
    });

    test("outsider cannot read answer — denied", async () => {
      await testEnv.withSecurityRulesDisabled(async (ctx) => {
        await setDoc(doc(ctx.firestore(), ANSWER_PATH), { userId: UID_A });
      });
      await assertFails(getDoc(doc(charlie().firestore(), ANSWER_PATH)));
    });

    test("create with extra field is denied", async () => {
      await seedThread();
      await assertFails(
        setDoc(doc(alice().firestore(), ANSWER_PATH), {
          userId: UID_A,
          questionId: "q1",
          answerType: "written",
          writtenText: CIPHERTEXT,
          createdAt: serverTimestamp(),
          updatedAt: serverTimestamp(),
          maliciousField: "injected",
        })
      );
    });
  });

  // ── messages ───────────────────────────────────────────────────────────────

  describe("messages/{messageId}", () => {
    const MSGS_PATH = `${THREAD_PATH}/messages`;

    test("member can send message with own authorUserId — allowed", async () => {
      await assertSucceeds(
        addDoc(collection(alice().firestore(), MSGS_PATH), {
          authorUserId: UID_A,
          text: CIPHERTEXT,
          createdAt: serverTimestamp(),
        })
      );
    });

    test("member cannot send message with other's authorUserId — denied", async () => {
      await assertFails(
        addDoc(collection(alice().firestore(), MSGS_PATH), {
          authorUserId: UID_B,
          text: "Impersonation attempt",
          createdAt: serverTimestamp(),
        })
      );
    });

    test("message without authorUserId — denied", async () => {
      await assertFails(
        addDoc(collection(alice().firestore(), MSGS_PATH), {
          userId: UID_A,
          text: "Old-style message",
          createdAt: serverTimestamp(),
        })
      );
    });

    test("outsider cannot send message — denied", async () => {
      await assertFails(
        addDoc(collection(charlie().firestore(), MSGS_PATH), {
          authorUserId: UID_C,
          text: "Intruder",
          createdAt: serverTimestamp(),
        })
      );
    });

    test("member can read messages — allowed", async () => {
      const msgPath = `${MSGS_PATH}/readable-message`;
      await testEnv.withSecurityRulesDisabled(async (ctx) => {
        await setDoc(doc(ctx.firestore(), msgPath), {
          authorUserId: UID_A,
          text: "Hi",
        });
      });
      await assertSucceeds(getDoc(doc(bob().firestore(), msgPath)));
    });

    test("author can update own message — allowed", async () => {
      const msgPath = `${MSGS_PATH}/owned-message`;
      await testEnv.withSecurityRulesDisabled(async (ctx) => {
        await setDoc(doc(ctx.firestore(), msgPath), {
          authorUserId: UID_A,
          text: CIPHERTEXT,
        });
      });
      await assertSucceeds(
        setDoc(doc(alice().firestore(), msgPath), { text: CIPHERTEXT })
      );
    });

    test("other member cannot update someone else's message — denied", async () => {
      const msgPath = `${MSGS_PATH}/other-message`;
      await testEnv.withSecurityRulesDisabled(async (ctx) => {
        await setDoc(doc(ctx.firestore(), msgPath), {
          authorUserId: UID_A,
          text: "Hi",
        });
      });
      await assertFails(
        updateDoc(doc(bob().firestore(), msgPath), { text: "Tampered" })
      );
    });

    test("create with extra field is denied", async () => {
      await seedThread();
      await assertFails(
        addDoc(collection(alice().firestore(), MSGS_PATH), {
          authorUserId: UID_A,
          text: CIPHERTEXT,
          createdAt: serverTimestamp(),
          maliciousField: "injected",
        })
      );
    });

    test("update with extra field is denied", async () => {
      const msgPath = `${MSGS_PATH}/extra-field-message`;
      await seedThread();
      await testEnv.withSecurityRulesDisabled(async (ctx) => {
        await setDoc(doc(ctx.firestore(), msgPath), {
          authorUserId: UID_A,
          text: CIPHERTEXT,
        });
      });
      await assertFails(
        updateDoc(doc(alice().firestore(), msgPath), {
          text: CIPHERTEXT,
          maliciousField: "injected",
        })
      );
    });
  });

  // ── reactions ──────────────────────────────────────────────────────────────

  describe("reactions/{reactionId}", () => {
    const REACTIONS_PATH = `${THREAD_PATH}/reactions`;

    test("member can add reaction with own userId — allowed", async () => {
      await seedThread();
      await assertSucceeds(
        setDoc(doc(alice().firestore(), `${REACTIONS_PATH}/${UID_A}_${UID_B}`), {
          userId: UID_A,
          emoji: "❤️",
          createdAt: serverTimestamp(),
        })
      );
    });

    test("member cannot add reaction with other's userId — denied", async () => {
      await assertFails(
        setDoc(doc(alice().firestore(), `${REACTIONS_PATH}/${UID_B}_${UID_A}`), {
          userId: UID_B,
          targetUserId: UID_A,
          emoji: "❤️",
          createdAt: serverTimestamp(),
        })
      );
    });

    test("create with extra field is denied", async () => {
      await seedThread();
      await assertFails(
        setDoc(doc(alice().firestore(), `${REACTIONS_PATH}/${UID_A}_${UID_B}`), {
          userId: UID_A,
          emoji: "❤️",
          createdAt: serverTimestamp(),
          maliciousField: "injected",
        })
      );
    });
  });
});

// ── couples/{coupleId}/date_swipes/{dateIdeaId} ──────────────────────────────

describe("couples/{coupleId}/date_swipes/{dateIdeaId}", () => {
  const SWIPE_PATH = `couples/${COUPLE_ID}/date_swipes/idea1`;

  beforeEach(seedCouple);

  test("member can write own swipe action — allowed", async () => {
    await assertSucceeds(
      setDoc(doc(alice().firestore(), SWIPE_PATH), {
        actions: {
          [UID_A]: { action: "love", swipedAt: Timestamp.now() },
        },
      })
    );
  });

  test("invalid swipe action value — denied", async () => {
    await assertFails(
      setDoc(doc(alice().firestore(), SWIPE_PATH), {
        actions: {
          [UID_A]: { action: "hate", swipedAt: Timestamp.now() },
        },
      })
    );
  });

  test("member cannot write other user's action — denied", async () => {
    await assertFails(
      setDoc(doc(alice().firestore(), SWIPE_PATH), {
        actions: {
          [UID_B]: { action: "love", swipedAt: Timestamp.now() },
        },
      })
    );
  });

  test("member can read swipes — allowed", async () => {
    await testEnv.withSecurityRulesDisabled(async (ctx) => {
      await setDoc(doc(ctx.firestore(), SWIPE_PATH), {
        actions: { [UID_A]: { action: "love", swipedAt: Timestamp.now() } },
      });
    });
    await assertSucceeds(getDoc(doc(bob().firestore(), SWIPE_PATH)));
  });

  test("client cannot delete swipe — denied", async () => {
    await testEnv.withSecurityRulesDisabled(async (ctx) => {
      await setDoc(doc(ctx.firestore(), SWIPE_PATH), {
        actions: { [UID_A]: { action: "love", swipedAt: Timestamp.now() } },
      });
    });
    await assertFails(deleteDoc(doc(alice().firestore(), SWIPE_PATH)));
  });
});

// ── couples/{coupleId}/date_matches/{matchId} ────────────────────────────────

describe("couples/{coupleId}/date_matches/{matchId}", () => {
  const MATCH_PATH = `couples/${COUPLE_ID}/date_matches/match1`;

  beforeEach(seedCouple);

  test("member can read match — allowed", async () => {
    await testEnv.withSecurityRulesDisabled(async (ctx) => {
      await setDoc(doc(ctx.firestore(), MATCH_PATH), { dateIdeaId: "idea1" });
    });
    await assertSucceeds(getDoc(doc(alice().firestore(), MATCH_PATH)));
  });

  test("client cannot create match (server-only) — denied", async () => {
    await assertFails(
      setDoc(doc(alice().firestore(), MATCH_PATH), { dateIdeaId: "idea1" })
    );
  });
});

// ── couples/{coupleId}/date_plans/{planId} ───────────────────────────────────

describe("couples/{coupleId}/date_plans/{planId}", () => {
  const PLAN_PATH = `couples/${COUPLE_ID}/date_plans/plan1`;

  beforeEach(seedCouple);

  const VALID_PLAN = {
    dateIdeaId: "idea1",
    scheduledDate: "2026-07-01",
    status: "draft",
    createdAt: Timestamp.now(),
    updatedAt: Timestamp.now(),
  };

  test("member can create date plan — allowed", async () => {
    await assertSucceeds(setDoc(doc(alice().firestore(), PLAN_PATH), VALID_PLAN));
  });

  test("invalid status on create — denied", async () => {
    await assertFails(
      setDoc(doc(alice().firestore(), PLAN_PATH), {
        ...VALID_PLAN,
        status: "cancelled",
      })
    );
  });

  test("member can update plan — allowed", async () => {
    await testEnv.withSecurityRulesDisabled(async (ctx) => {
      await setDoc(doc(ctx.firestore(), PLAN_PATH), VALID_PLAN);
    });
    await assertSucceeds(
      updateDoc(doc(alice().firestore(), PLAN_PATH), {
        status: "planned",
        updatedAt: Timestamp.now(),
      })
    );
  });

  test("member can delete plan — allowed", async () => {
    await testEnv.withSecurityRulesDisabled(async (ctx) => {
      await setDoc(doc(ctx.firestore(), PLAN_PATH), VALID_PLAN);
    });
    await assertSucceeds(deleteDoc(doc(alice().firestore(), PLAN_PATH)));
  });
});

// ── couples/{coupleId}/bucket_list/{itemId} ──────────────────────────────────

describe("couples/{coupleId}/bucket_list/{itemId}", () => {
  const ITEM_PATH = `couples/${COUPLE_ID}/bucket_list/item1`;

  beforeEach(seedCouple);

  const VALID_ITEM = {
    title: "Skydiving",
    addedBy: UID_A,
    addedAt: Timestamp.now(),
    isCompleted: false,
    category: "adventure",
  };

  test("member can create bucket list item — allowed", async () => {
    await assertSucceeds(setDoc(doc(alice().firestore(), ITEM_PATH), VALID_ITEM));
  });

  test("creator must match addedBy — denied", async () => {
    await assertFails(
      setDoc(doc(alice().firestore(), ITEM_PATH), { ...VALID_ITEM, addedBy: UID_B })
    );
  });

  test("invalid category — denied", async () => {
    await assertFails(
      setDoc(doc(alice().firestore(), ITEM_PATH), { ...VALID_ITEM, category: "other" })
    );
  });

  test("member can mark item complete (own completedBy) — allowed", async () => {
    await testEnv.withSecurityRulesDisabled(async (ctx) => {
      await setDoc(doc(ctx.firestore(), ITEM_PATH), VALID_ITEM);
    });
    await assertSucceeds(
      updateDoc(doc(bob().firestore(), ITEM_PATH), {
        isCompleted: true,
        completedBy: UID_B,
        completedAt: Timestamp.now(),
      })
    );
  });

  test("member cannot mark complete with other's completedBy — denied", async () => {
    await testEnv.withSecurityRulesDisabled(async (ctx) => {
      await setDoc(doc(ctx.firestore(), ITEM_PATH), VALID_ITEM);
    });
    await assertFails(
      updateDoc(doc(alice().firestore(), ITEM_PATH), {
        isCompleted: true,
        completedBy: UID_B,
        completedAt: Timestamp.now(),
      })
    );
  });

  test("addedBy is immutable — denied", async () => {
    await testEnv.withSecurityRulesDisabled(async (ctx) => {
      await setDoc(doc(ctx.firestore(), ITEM_PATH), VALID_ITEM);
    });
    await assertFails(
      updateDoc(doc(alice().firestore(), ITEM_PATH), { addedBy: UID_B })
    );
  });

  test("member can delete item — allowed", async () => {
    await testEnv.withSecurityRulesDisabled(async (ctx) => {
      await setDoc(doc(ctx.firestore(), ITEM_PATH), VALID_ITEM);
    });
    await assertSucceeds(deleteDoc(doc(alice().firestore(), ITEM_PATH)));
  });
});

// ── couples/{coupleId}/daily_question/{date} ─────────────────────────────────

describe("couples/{coupleId}/daily_question/{date}", () => {
  const DQ_PATH = `couples/${COUPLE_ID}/daily_question/2026-06-19`;

  beforeEach(seedCouple);

  test("member can read daily question — allowed", async () => {
    await testEnv.withSecurityRulesDisabled(async (ctx) => {
      await setDoc(doc(ctx.firestore(), DQ_PATH), { questionId: "q42" });
    });
    await assertSucceeds(getDoc(doc(alice().firestore(), DQ_PATH)));
  });

  test("client cannot write daily question (server-only) — denied", async () => {
    await assertFails(
      setDoc(doc(alice().firestore(), DQ_PATH), { questionId: "q42" })
    );
  });

  // ── daily_question answers ──────────────────────────────────────────────────

  describe("answers/{userId}", () => {
    const ANSWER_PATH = `${DQ_PATH}/answers/${UID_A}`;

    beforeEach(async () => {
      await testEnv.withSecurityRulesDisabled(async (ctx) => {
        await setDoc(doc(ctx.firestore(), DQ_PATH), { questionId: "q42" });
      });
    });

    test("owner can create own daily answer — allowed", async () => {
      await assertSucceeds(
        setDoc(doc(alice().firestore(), ANSWER_PATH), {
          userId: UID_A,
          questionId: "q42",
          answerType: "written",
          createdAt: serverTimestamp(),
          updatedAt: serverTimestamp(),
        })
      );
    });

    test("owner cannot create answer for another user's path — denied", async () => {
      await assertFails(
        setDoc(doc(alice().firestore(), `${DQ_PATH}/answers/${UID_B}`), {
          userId: UID_B,
          questionId: "q42",
          answerType: "written",
          createdAt: serverTimestamp(),
          updatedAt: serverTimestamp(),
        })
      );
    });

    test("partner can read daily answer — allowed", async () => {
      await testEnv.withSecurityRulesDisabled(async (ctx) => {
        await setDoc(doc(ctx.firestore(), ANSWER_PATH), {
          userId: UID_A,
          questionId: "q42",
          answerType: "written",
        });
      });
      await assertSucceeds(getDoc(doc(bob().firestore(), ANSWER_PATH)));
    });

    test("outsider cannot read daily answer — denied", async () => {
      await testEnv.withSecurityRulesDisabled(async (ctx) => {
        await setDoc(doc(ctx.firestore(), ANSWER_PATH), { userId: UID_A });
      });
      await assertFails(getDoc(doc(charlie().firestore(), ANSWER_PATH)));
    });

    test("client cannot delete daily answer — denied", async () => {
      await testEnv.withSecurityRulesDisabled(async (ctx) => {
        await setDoc(doc(ctx.firestore(), ANSWER_PATH), { userId: UID_A });
      });
      await assertFails(deleteDoc(doc(alice().firestore(), ANSWER_PATH)));
    });

    test("create with extra field is denied", async () => {
      await assertFails(
        setDoc(doc(alice().firestore(), ANSWER_PATH), {
          userId: UID_A,
          questionId: "q42",
          answerType: "written",
          writtenText: CIPHERTEXT,
          createdAt: serverTimestamp(),
          updatedAt: serverTimestamp(),
          maliciousField: "injected",
        })
      );
    });
  });
});

// ── Private game answers ────────────────────────────────────────────────────

describe.each(["this_or_that", "desire_sync", "how_well", "wheel"])(
  "couples/{coupleId}/%s/{sessionId}",
  (gameCollection) => {
    const gamePath = () => `couples/${COUPLE_ID}/${gameCollection}/session1`;

    beforeEach(seedCouple);

    test("a member can submit an encrypted answer — allowed", async () => {
      await assertSucceeds(setDoc(doc(alice().firestore(), gamePath()), {
        answers: { [UID_A]: CIPHERTEXT },
      }));
    });

    test("a plaintext answer is rejected — denied", async () => {
      await assertFails(setDoc(doc(alice().firestore(), gamePath()), {
        answers: { [UID_A]: "private answer" },
      }));
    });

    test("a partner cannot overwrite the other user's answer — denied", async () => {
      await testEnv.withSecurityRulesDisabled(async (ctx) => {
        await setDoc(doc(ctx.firestore(), gamePath()), {
          answers: { [UID_A]: CIPHERTEXT },
        });
      });

      await assertFails(updateDoc(doc(bob().firestore(), gamePath()), {
        [`answers.${UID_A}`]: "enc:v1:ZGVm",
      }));
    });

    test("a partner can add their own encrypted answer — allowed", async () => {
      await testEnv.withSecurityRulesDisabled(async (ctx) => {
        await setDoc(doc(ctx.firestore(), gamePath()), {
          answers: { [UID_A]: CIPHERTEXT },
        });
      });

      await assertSucceeds(updateDoc(doc(bob().firestore(), gamePath()), {
        [`answers.${UID_B}`]: "enc:v1:ZGVm",
      }));
    });
  }
);

// ── entitlement_events/{eventId} ─────────────────────────────────────────────

describe("entitlement_events/{eventId}", () => {
  test("no client read — denied", async () => {
    await assertFails(
      getDoc(doc(alice().firestore(), "entitlement_events/event1"))
    );
  });

  test("no client write — denied", async () => {
    await assertFails(
      setDoc(doc(alice().firestore(), "entitlement_events/event1"), {
        processed: true,
      })
    );
  });
});
