import { convexTest } from "convex-test";
import { anyApi } from "convex/server";
import { describe, expect, test } from "vitest";

import { requireOrganizationMember } from "./authz";
import schema from "./schema";

// `import.meta.glob` is provided by the Vitest/Vite test runner at runtime; it
// has no type definition under the Convex tsconfig (which scopes `types` to
// node), so declare the minimal shape the test relies on.
declare global {
  interface ImportMeta {
    readonly glob: (pattern: string) => Record<string, () => Promise<unknown>>;
  }
}

const modules = import.meta.glob("./**/*.ts");
const api = anyApi;

const ID_A = "https://convex.test|user-a";
const ID_B = "https://convex.test|user-b";

const identityA = { tokenIdentifier: ID_A };
const identityB = { tokenIdentifier: ID_B };

const newTest = () => convexTest({ schema, modules });

describe("organizations", () => {
  test("first ensure creates one organization and owner membership", async () => {
    const t = newTest();
    const org = await t
      .withIdentity(identityA)
      .mutation(api.organizations.ensurePersonalOrganization, {});

    expect(org.kind).toBe("personal");
    expect(org.name).toBe("Personal");
    expect(org.createdBy).toBe(ID_A);
    expect(org._id).toBeTruthy();

    // Idempotent shape: a second ensure returns the same organization.
    const again = await t
      .withIdentity(identityA)
      .mutation(api.organizations.ensurePersonalOrganization, {});
    expect(again._id).toBe(org._id);

    // The current-organization query reflects the ensured org.
    const current = await t
      .withIdentity(identityA)
      .query(api.organizations.getCurrent, {});
    expect(current?._id).toBe(org._id);
  });

  test("repeated ensure returns the same organization without duplicates", async () => {
    const t = newTest();

    const first = await t
      .withIdentity(identityA)
      .mutation(api.organizations.ensurePersonalOrganization, {});

    // Many subsequent ensures must all resolve to the single org.
    const repeats = await Promise.all(
      Array.from({ length: 5 }, () =>
        t
          .withIdentity(identityA)
          .mutation(api.organizations.ensurePersonalOrganization, {})
      )
    );

    for (const org of repeats) {
      expect(org._id).toBe(first._id);
    }

    // The current-organization query still reports the single org.
    const allForA = await t
      .withIdentity(identityA)
      .query(api.organizations.getCurrent, {});
    expect(allForA?._id).toBe(first._id);
  });

  test("unauthenticated access is denied", async () => {
    const t = newTest();

    await expect(t.query(api.organizations.getCurrent, {})).rejects.toThrow(
      /Authentication required/u
    );

    await expect(
      t.mutation(api.organizations.ensurePersonalOrganization, {})
    ).rejects.toThrow(/Authentication required/u);
  });

  test("a second identity cannot read or mutate the first organization", async () => {
    const t = newTest();

    const orgA = await t
      .withIdentity(identityA)
      .mutation(api.organizations.ensurePersonalOrganization, {});

    // The second identity sees none of the first identity's organizations.
    const currentForB = await t
      .withIdentity(identityB)
      .query(api.organizations.getCurrent, {});
    expect(currentForB).toBeNull();

    // The second identity gets its own distinct organization.
    const orgB = await t
      .withIdentity(identityB)
      .mutation(api.organizations.ensurePersonalOrganization, {});
    expect(orgB._id).not.toBe(orgA._id);

    // The membership boundary denies the second identity access to org A.
    await expect(
      t
        .withIdentity(identityB)
        .query((ctx) => requireOrganizationMember(ctx, orgA._id))
    ).rejects.toThrow(/Organization membership required/u);

    // But the first identity is a confirmed member of its own organization.
    await expect(
      t
        .withIdentity(identityA)
        .query((ctx) => requireOrganizationMember(ctx, orgA._id))
    ).resolves.toBe(ID_A);
  });
});
