/**
 * Auth Tables RLS Integration Tests
 *
 * Tests that RLS policies on accounts and sessions tables correctly
 * enforce user-scoped access and prevent cross-user data leakage.
 *
 * Related: #350 - Add RLS policies to auth tables with FORCE enforcement
 */

import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { PrismaClient, Prisma } from "@prisma/client";
import { randomUUID as uuid } from "crypto";
import { runWithRlsClient, getRlsClient } from "../prisma/rls-context.provider";

const shouldRunDbIntegrationTests =
  process.env.RUN_DB_TESTS === "true" && Boolean(process.env.DATABASE_URL);

describe.skipIf(!shouldRunDbIntegrationTests)(
  "Auth Tables RLS Policies (requires DATABASE_URL)",
  () => {
    let prisma: PrismaClient;
    const testData: {
      users: string[];
      accounts: string[];
      sessions: string[];
    } = {
      users: [],
      accounts: [],
      sessions: [],
    };

    beforeAll(async () => {
      // Skip setup if DATABASE_URL is not available
      if (!shouldRunDbIntegrationTests) {
        return;
      }

      prisma = new PrismaClient();
      await prisma.$connect();

      // RLS policies are bypassed for superusers
      const [{ rolsuper }] = await prisma.$queryRaw<[{ rolsuper: boolean }]>`
        SELECT rolsuper FROM pg_roles WHERE rolname = current_user
      `;
      if (rolsuper) {
        throw new Error(
          "Auth RLS integration tests require a non-superuser database role. " +
            "See migration 20260207_add_auth_rls_policies for setup instructions."
        );
      }
    });

    afterAll(async () => {
      // Skip cleanup if DATABASE_URL is not available or prisma not initialized
      if (!shouldRunDbIntegrationTests || !prisma) {
        return;
      }

      try {
        // Clean up test data
        if (testData.sessions.length > 0) {
          await prisma.session.deleteMany({
            where: { id: { in: testData.sessions } },
          });
        }

        if (testData.accounts.length > 0) {
          await prisma.account.deleteMany({
            where: { id: { in: testData.accounts } },
          });
        }

        if (testData.users.length > 0) {
          await prisma.user.deleteMany({
            where: { id: { in: testData.users } },
          });
        }

        await prisma.$disconnect();
      } catch (error) {
        console.error(
          "Test cleanup failed:",
          error instanceof Error ? error.message : String(error)
        );
        // Re-throw to make test failure visible
        throw new Error(
          "Test cleanup failed. Database may contain orphaned test data. " +
            `Error: ${error instanceof Error ? error.message : String(error)}`
        );
      }
    });

    async function createTestUser(email: string): Promise<string> {
      const userId = uuid();
      await prisma.user.create({
        data: {
          id: userId,
          email,
          name: `Test User ${email}`,
          authProviderId: `auth-${userId}`,
        },
      });
      testData.users.push(userId);
      return userId;
    }

    async function createTestAccount(userId: string, token: string): Promise<string> {
      const accountId = uuid();
      await prisma.account.create({
        data: {
          id: accountId,
          userId,
          accountId: `account-${accountId}`,
          providerId: "test-provider",
          accessToken: token,
        },
      });
      testData.accounts.push(accountId);
      return accountId;
    }

    async function createTestSession(userId: string): Promise<string> {
      const sessionId = uuid();
      await prisma.session.create({
        data: {
          id: sessionId,
          userId,
          token: `session-${sessionId}-${Date.now()}`,
          expiresAt: new Date(Date.now() + 86400000),
        },
      });
      testData.sessions.push(sessionId);
      return sessionId;
    }

    describe("Account table RLS", () => {
      it("should allow user to read their own accounts when RLS context is set", async () => {
        const user1Id = await createTestUser("account-read-own@test.com");
        const account1Id = await createTestAccount(user1Id, "user1-token");

        // Use runWithRlsClient to set RLS context
        const result = await prisma.$transaction(async (tx) => {
          await tx.$executeRaw`SELECT set_config('app.current_user_id', ${user1Id}::text, true)`;

          return runWithRlsClient(tx, async () => {
            const client = getRlsClient()!;
            const accounts = await client.account.findMany({
              where: { userId: user1Id },
            });

            return accounts;
          });
        });

        expect(result).toHaveLength(1);
        expect(result[0].id).toBe(account1Id);
        expect(result[0].accessToken).toBe("user1-token");
      });

      it("should prevent user from reading other users accounts", async () => {
        const user1Id = await createTestUser("account-read-self@test.com");
        const user2Id = await createTestUser("account-read-other@test.com");
        await createTestAccount(user1Id, "user1-token");
        await createTestAccount(user2Id, "user2-token");

        // Set RLS context for user1, try to read user2's accounts
        const result = await prisma.$transaction(async (tx) => {
          await tx.$executeRaw`SELECT set_config('app.current_user_id', ${user1Id}::text, true)`;

          return runWithRlsClient(tx, async () => {
            const client = getRlsClient()!;
            const accounts = await client.account.findMany({
              where: { userId: user2Id },
            });

            return accounts;
          });
        });

        // Should return empty array due to RLS policy
        expect(result).toHaveLength(0);
      });

      it("should prevent direct access by ID to other users accounts", async () => {
        const user1Id = await createTestUser("account-id-self@test.com");
        const user2Id = await createTestUser("account-id-other@test.com");
        await createTestAccount(user1Id, "user1-token");
        const account2Id = await createTestAccount(user2Id, "user2-token");

        // Set RLS context for user1, try to read user2's account by ID
        const result = await prisma.$transaction(async (tx) => {
          await tx.$executeRaw`SELECT set_config('app.current_user_id', ${user1Id}::text, true)`;

          return runWithRlsClient(tx, async () => {
            const client = getRlsClient()!;
            const account = await client.account.findUnique({
              where: { id: account2Id },
            });

            return account;
          });
        });

        // Should return null due to RLS policy
        expect(result).toBeNull();
      });

      it("should allow user to create their own accounts", async () => {
        const user1Id = await createTestUser("account-create-own@test.com");

        // Set RLS context for user1, create their own account
        const result = await prisma.$transaction(async (tx) => {
          await tx.$executeRaw`SELECT set_config('app.current_user_id', ${user1Id}::text, true)`;

          return runWithRlsClient(tx, async () => {
            const client = getRlsClient()!;
            const newAccount = await client.account.create({
              data: {
                id: uuid(),
                userId: user1Id,
                accountId: "new-account",
                providerId: "test-provider",
                accessToken: "new-token",
              },
            });

            testData.accounts.push(newAccount.id);
            return newAccount;
          });
        });

        expect(result).toBeDefined();
        expect(result.userId).toBe(user1Id);
        expect(result.accessToken).toBe("new-token");
      });

      it("should prevent user from creating accounts for other users", async () => {
        const user1Id = await createTestUser("account-create-self@test.com");
        const user2Id = await createTestUser("account-create-other@test.com");

        // Set RLS context for user1, try to create an account for user2
        await expect(
          prisma.$transaction(async (tx) => {
            await tx.$executeRaw`SELECT set_config('app.current_user_id', ${user1Id}::text, true)`;

            return runWithRlsClient(tx, async () => {
              const client = getRlsClient()!;
              const newAccount = await client.account.create({
                data: {
                  id: uuid(),
                  userId: user2Id, // Trying to create for user2 while logged in as user1
                  accountId: "hacked-account",
                  providerId: "test-provider",
                  accessToken: "hacked-token",
                },
              });

              testData.accounts.push(newAccount.id);
              return newAccount;
            });
          })
        ).rejects.toThrow();
      });

      it("should allow user to update their own accounts", async () => {
        const user1Id = await createTestUser("account-update-own@test.com");
        const account1Id = await createTestAccount(user1Id, "original-token");

        const result = await prisma.$transaction(async (tx) => {
          await tx.$executeRaw`SELECT set_config('app.current_user_id', ${user1Id}::text, true)`;

          return runWithRlsClient(tx, async () => {
            const client = getRlsClient()!;
            const updated = await client.account.update({
              where: { id: account1Id },
              data: { accessToken: "updated-token" },
            });

            return updated;
          });
        });

        expect(result.accessToken).toBe("updated-token");
      });

      it("should prevent user from updating other users accounts", async () => {
        const user1Id = await createTestUser("account-update-self@test.com");
        const user2Id = await createTestUser("account-update-other@test.com");
        await createTestAccount(user1Id, "user1-token");
        const account2Id = await createTestAccount(user2Id, "user2-token");

        // Set RLS context for user1, try to update user2's account
        await expect(
          prisma.$transaction(async (tx) => {
            await tx.$executeRaw`SELECT set_config('app.current_user_id', ${user1Id}::text, true)`;

            return runWithRlsClient(tx, async () => {
              const client = getRlsClient()!;
              await client.account.update({
                where: { id: account2Id },
                data: { accessToken: "hacked-token" },
              });
            });
          })
        ).rejects.toThrow();
      });

      it("should prevent user from deleting other users accounts", async () => {
        const user1Id = await createTestUser("account-delete-self@test.com");
        const user2Id = await createTestUser("account-delete-other@test.com");
        await createTestAccount(user1Id, "user1-token");
        const account2Id = await createTestAccount(user2Id, "user2-token");

        // Set RLS context for user1, try to delete user2's account
        await expect(
          prisma.$transaction(async (tx) => {
            await tx.$executeRaw`SELECT set_config('app.current_user_id', ${user1Id}::text, true)`;

            return runWithRlsClient(tx, async () => {
              const client = getRlsClient()!;
              await client.account.delete({
                where: { id: account2Id },
              });
            });
          })
        ).rejects.toThrow();

        // Verify the record still exists and wasn't deleted
        const stillExists = await prisma.account.findUnique({ where: { id: account2Id } });
        expect(stillExists).not.toBeNull();
        expect(stillExists?.userId).toBe(user2Id);
      });

      it("should allow user to delete their own accounts", async () => {
        const user1Id = await createTestUser("account-delete-own@test.com");
        const account1Id = await createTestAccount(user1Id, "user1-token");

        // Set RLS context for user1, delete their own account
        await prisma.$transaction(async (tx) => {
          await tx.$executeRaw`SELECT set_config('app.current_user_id', ${user1Id}::text, true)`;

          return runWithRlsClient(tx, async () => {
            const client = getRlsClient()!;
            await client.account.delete({
              where: { id: account1Id },
            });
          });
        });

        // Verify the record was actually deleted
        const deleted = await prisma.account.findUnique({ where: { id: account1Id } });
        expect(deleted).toBeNull();
      });
    });

    describe("Session table RLS", () => {
      it("should allow user to read their own sessions when RLS context is set", async () => {
        const user1Id = await createTestUser("session-read-own@test.com");
        const session1Id = await createTestSession(user1Id);

        const result = await prisma.$transaction(async (tx) => {
          await tx.$executeRaw`SELECT set_config('app.current_user_id', ${user1Id}::text, true)`;

          return runWithRlsClient(tx, async () => {
            const client = getRlsClient()!;
            const sessions = await client.session.findMany({
              where: { userId: user1Id },
            });

            return sessions;
          });
        });

        expect(result).toHaveLength(1);
        expect(result[0].id).toBe(session1Id);
      });

      it("should prevent user from reading other users sessions", async () => {
        const user1Id = await createTestUser("session-read-self@test.com");
        const user2Id = await createTestUser("session-read-other@test.com");
        await createTestSession(user1Id);
        await createTestSession(user2Id);

        const result = await prisma.$transaction(async (tx) => {
          await tx.$executeRaw`SELECT set_config('app.current_user_id', ${user1Id}::text, true)`;

          return runWithRlsClient(tx, async () => {
            const client = getRlsClient()!;
            const sessions = await client.session.findMany({
              where: { userId: user2Id },
            });

            return sessions;
          });
        });

        // Should return empty array due to RLS policy
        expect(result).toHaveLength(0);
      });

      it("should prevent direct access by ID to other users sessions", async () => {
        const user1Id = await createTestUser("session-id-self@test.com");
        const user2Id = await createTestUser("session-id-other@test.com");
        await createTestSession(user1Id);
        const session2Id = await createTestSession(user2Id);

        const result = await prisma.$transaction(async (tx) => {
          await tx.$executeRaw`SELECT set_config('app.current_user_id', ${user1Id}::text, true)`;

          return runWithRlsClient(tx, async () => {
            const client = getRlsClient()!;
            const session = await client.session.findUnique({
              where: { id: session2Id },
            });

            return session;
          });
        });

        // Should return null due to RLS policy
        expect(result).toBeNull();
      });

      it("should allow user to create their own sessions", async () => {
        const user1Id = await createTestUser("session-create-own@test.com");

        // Set RLS context for user1, create their own session
        const result = await prisma.$transaction(async (tx) => {
          await tx.$executeRaw`SELECT set_config('app.current_user_id', ${user1Id}::text, true)`;

          return runWithRlsClient(tx, async () => {
            const client = getRlsClient()!;
            const newSession = await client.session.create({
              data: {
                id: uuid(),
                userId: user1Id,
                token: `new-session-${Date.now()}`,
                expiresAt: new Date(Date.now() + 86400000),
              },
            });

            testData.sessions.push(newSession.id);
            return newSession;
          });
        });

        expect(result).toBeDefined();
        expect(result.userId).toBe(user1Id);
        expect(result.token).toContain("new-session");
      });

      it("should prevent user from creating sessions for other users", async () => {
        const user1Id = await createTestUser("session-create-self@test.com");
        const user2Id = await createTestUser("session-create-other@test.com");

        // Set RLS context for user1, try to create a session for user2
        await expect(
          prisma.$transaction(async (tx) => {
            await tx.$executeRaw`SELECT set_config('app.current_user_id', ${user1Id}::text, true)`;

            return runWithRlsClient(tx, async () => {
              const client = getRlsClient()!;
              const newSession = await client.session.create({
                data: {
                  id: uuid(),
                  userId: user2Id, // Trying to create for user2 while logged in as user1
                  token: `hacked-session-${Date.now()}`,
                  expiresAt: new Date(Date.now() + 86400000),
                },
              });

              testData.sessions.push(newSession.id);
              return newSession;
            });
          })
        ).rejects.toThrow();
      });

      it("should allow user to update their own sessions", async () => {
        const user1Id = await createTestUser("session-update-own@test.com");
        const session1Id = await createTestSession(user1Id);

        const result = await prisma.$transaction(async (tx) => {
          await tx.$executeRaw`SELECT set_config('app.current_user_id', ${user1Id}::text, true)`;

          return runWithRlsClient(tx, async () => {
            const client = getRlsClient()!;
            const updated = await client.session.update({
              where: { id: session1Id },
              data: { ipAddress: "192.168.1.1" },
            });

            return updated;
          });
        });

        expect(result.ipAddress).toBe("192.168.1.1");
      });

      it("should prevent user from updating other users sessions", async () => {
        const user1Id = await createTestUser("session-update-self@test.com");
        const user2Id = await createTestUser("session-update-other@test.com");
        await createTestSession(user1Id);
        const session2Id = await createTestSession(user2Id);

        await expect(
          prisma.$transaction(async (tx) => {
            await tx.$executeRaw`SELECT set_config('app.current_user_id', ${user1Id}::text, true)`;

            return runWithRlsClient(tx, async () => {
              const client = getRlsClient()!;
              await client.session.update({
                where: { id: session2Id },
                data: { ipAddress: "10.0.0.1" },
              });
            });
          })
        ).rejects.toThrow();
      });

      it("should prevent user from deleting other users sessions", async () => {
        const user1Id = await createTestUser("session-delete-self@test.com");
        const user2Id = await createTestUser("session-delete-other@test.com");
        await createTestSession(user1Id);
        const session2Id = await createTestSession(user2Id);

        await expect(
          prisma.$transaction(async (tx) => {
            await tx.$executeRaw`SELECT set_config('app.current_user_id', ${user1Id}::text, true)`;

            return runWithRlsClient(tx, async () => {
              const client = getRlsClient()!;
              await client.session.delete({
                where: { id: session2Id },
              });
            });
          })
        ).rejects.toThrow();

        // Verify the record still exists and wasn't deleted
        const stillExists = await prisma.session.findUnique({ where: { id: session2Id } });
        expect(stillExists).not.toBeNull();
        expect(stillExists?.userId).toBe(user2Id);
      });

      it("should allow user to delete their own sessions", async () => {
        const user1Id = await createTestUser("session-delete-own@test.com");
        const session1Id = await createTestSession(user1Id);

        // Set RLS context for user1, delete their own session
        await prisma.$transaction(async (tx) => {
          await tx.$executeRaw`SELECT set_config('app.current_user_id', ${user1Id}::text, true)`;

          return runWithRlsClient(tx, async () => {
            const client = getRlsClient()!;
            await client.session.delete({
              where: { id: session1Id },
            });
          });
        });

        // Verify the record was actually deleted
        const deleted = await prisma.session.findUnique({ where: { id: session1Id } });
        expect(deleted).toBeNull();
      });
    });

    describe("Owner bypass policy", () => {
      it("should allow table owner to access all records without RLS context", async () => {
        const user1Id = await createTestUser("owner-bypass-1@test.com");
        const user2Id = await createTestUser("owner-bypass-2@test.com");
        const account1Id = await createTestAccount(user1Id, "token1");
        const account2Id = await createTestAccount(user2Id, "token2");

        // Don't set RLS context - rely on owner bypass policy
        const accounts = await prisma.account.findMany({
          where: {
            id: { in: [account1Id, account2Id] },
          },
        });

        // Owner should see both accounts
        expect(accounts).toHaveLength(2);
      });

      it("should allow migrations to work without RLS context", async () => {
        const userId = await createTestUser("migration-test@test.com");

        // This simulates a migration or BetterAuth internal operation
        // that doesn't have RLS context set
        const newAccount = await prisma.account.create({
          data: {
            id: uuid(),
            userId,
            accountId: "migration-test-account",
            providerId: "test-migration",
          },
        });

        expect(newAccount.id).toBeDefined();

        // Clean up
        await prisma.account.delete({
          where: { id: newAccount.id },
        });
      });
    });

    describe("RLS context isolation", () => {
      it("should enforce RLS when context is set, even for table owner", async () => {
        const user1Id = await createTestUser("rls-enforce-1@test.com");
        const user2Id = await createTestUser("rls-enforce-2@test.com");
        const account1Id = await createTestAccount(user1Id, "token1");
        const account2Id = await createTestAccount(user2Id, "token2");

        // With RLS context set for user1, they should only see their own account
        const result = await prisma.$transaction(async (tx) => {
          await tx.$executeRaw`SELECT set_config('app.current_user_id', ${user1Id}::text, true)`;

          return runWithRlsClient(tx, async () => {
            const client = getRlsClient()!;
            const accounts = await client.account.findMany({
              where: {
                id: { in: [account1Id, account2Id] },
              },
            });

            return accounts;
          });
        });

        // Should only see user1's account, not user2's
        expect(result).toHaveLength(1);
        expect(result[0].id).toBe(account1Id);
      });

      it("should allow different users to see only their own data in separate contexts", async () => {
        const user1Id = await createTestUser("context-user1@test.com");
        const user2Id = await createTestUser("context-user2@test.com");
        const session1Id = await createTestSession(user1Id);
        const session2Id = await createTestSession(user2Id);

        // User1 context - query for both sessions, but RLS should only return user1's
        const user1Result = await prisma.$transaction(async (tx) => {
          await tx.$executeRaw`SELECT set_config('app.current_user_id', ${user1Id}::text, true)`;

          return runWithRlsClient(tx, async () => {
            const client = getRlsClient()!;
            return client.session.findMany({
              where: {
                id: { in: [session1Id, session2Id] },
              },
            });
          });
        });

        // User2 context - query for both sessions, but RLS should only return user2's
        const user2Result = await prisma.$transaction(async (tx) => {
          await tx.$executeRaw`SELECT set_config('app.current_user_id', ${user2Id}::text, true)`;

          return runWithRlsClient(tx, async () => {
            const client = getRlsClient()!;
            return client.session.findMany({
              where: {
                id: { in: [session1Id, session2Id] },
              },
            });
          });
        });

        // Each user should only see their own session
        expect(user1Result).toHaveLength(1);
        expect(user1Result[0].id).toBe(session1Id);

        expect(user2Result).toHaveLength(1);
        expect(user2Result[0].id).toBe(session2Id);
      });
    });
  }
);
