/**
 * Mobile Authentication Integration Tests
 *
 * Tests mobile pairing, token exchange, refresh, and session management
 * against a real database.
 *
 * Run with: pnpm test:integration
 */

import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest';
import Fastify, { type FastifyInstance } from 'fastify';
import jwt from '@fastify/jwt';
import cookie from '@fastify/cookie';
import sensible from '@fastify/sensible';
import { createHash, randomBytes } from 'crypto';
import { eq } from 'drizzle-orm';
import type { Redis } from 'ioredis';
import type { AuthUser } from '@tracearr/shared';
import { db } from '../../src/db/client.js';
import { users, servers, serverUsers, settings, mobileTokens, mobileSessions } from '../../src/db/schema.js';
import { mobileRoutes } from '../../src/routes/mobile.js';

// Constants (matching mobile.ts)
const TOKEN_EXPIRY_MINUTES = 15;
const MOBILE_TOKEN_PREFIX = 'trr_mob_';
const MOBILE_REFRESH_TTL = 90 * 24 * 60 * 60; // 90 days

// Test helpers
function hashToken(token: string): string {
  return createHash('sha256').update(token).digest('hex');
}

function generateTestMobileToken(): string {
  const randomPart = randomBytes(32).toString('base64url');
  return `${MOBILE_TOKEN_PREFIX}${randomPart}`;
}

// Create mock Redis for rate limiting
function createMockRedis() {
  const store = new Map<string, string>();
  const counters = new Map<string, number>();

  return {
    get: vi.fn(async (key: string) => store.get(key) ?? null),
    set: vi.fn(async (key: string, value: string) => {
      store.set(key, value);
      return 'OK';
    }),
    setex: vi.fn(async (key: string, _seconds: number, value: string) => {
      store.set(key, value);
      return 'OK';
    }),
    del: vi.fn(async (key: string) => {
      store.delete(key);
      return 1;
    }),
    incr: vi.fn(async (key: string) => {
      const current = counters.get(key) ?? 0;
      counters.set(key, current + 1);
      return current + 1;
    }),
    expire: vi.fn(async () => 1),
    ttl: vi.fn(async () => 300),
    ping: vi.fn(async () => 'PONG'),
    keys: vi.fn(async (pattern: string) => {
      const prefix = pattern.replace('*', '');
      return Array.from(store.keys()).filter(k => k.startsWith(prefix));
    }),
    eval: vi.fn(async () => 1), // Default: first attempt (not rate limited)
    _store: store,
    _counters: counters,
    _reset: () => {
      store.clear();
      counters.clear();
    },
  };
}

// Test data holder
interface TestData {
  ownerId: string;
  serverId: string;
  serverUserId: string;
}

// Create test Fastify app with mobile routes
async function createMobileTestApp(): Promise<FastifyInstance> {
  const app = Fastify({
    logger: false,
  });

  // Register essential plugins
  await app.register(sensible);
  await app.register(cookie, { secret: 'test-cookie-secret-32-chars-long!' });
  await app.register(jwt, {
    secret: process.env.JWT_SECRET ?? 'test-jwt-secret-must-be-32-chars-min',
    sign: { algorithm: 'HS256' },
  });

  // Add mock Redis
  const mockRedis = createMockRedis();
  app.decorate('redis', mockRedis as unknown as Redis);

  // Add authenticate decorator
  app.decorate('authenticate', async function (request: any, reply: any) {
    try {
      await request.jwtVerify();
    } catch {
      reply.unauthorized('Invalid or expired token');
    }
  });

  // Add requireOwner decorator
  app.decorate('requireOwner', async function (request: any, reply: any) {
    try {
      await request.jwtVerify();
      if (request.user.role !== 'owner') {
        reply.forbidden('Owner access required');
      }
    } catch {
      reply.unauthorized('Invalid or expired token');
    }
  });

  // Add requireMobile decorator - validates token was issued for mobile app
  app.decorate('requireMobile', async function (request: any, reply: any) {
    try {
      await request.jwtVerify();
      if (!request.user.mobile) {
        reply.forbidden('Mobile access token required');
      }
    } catch {
      reply.unauthorized('Invalid or expired token');
    }
  });

  // Register mobile routes (same prefix as server)
  await app.register(mobileRoutes, { prefix: '/api/v1/mobile' });

  return app;
}

// Seed test data
async function seedTestData(): Promise<TestData> {
  // Create owner user
  const [user] = await db
    .insert(users)
    .values({
      username: 'testowner',
      name: 'Test Owner',
      role: 'owner',
      aggregateTrustScore: 100,
    })
    .returning();

  // Create server
  const [server] = await db
    .insert(servers)
    .values({
      name: 'Test Plex Server',
      type: 'plex',
      url: 'http://localhost:32400',
      token: 'test-token-encrypted',
    })
    .returning();

  // Create server_user
  const [serverUser] = await db
    .insert(serverUsers)
    .values({
      userId: user.id,
      serverId: server.id,
      externalId: 'plex-user-1',
      username: 'testowner',
      isServerAdmin: true,
      trustScore: 100,
    })
    .returning();

  // Ensure settings row exists with mobile enabled
  await db
    .insert(settings)
    .values({ id: 1, mobileEnabled: true })
    .onConflictDoUpdate({
      target: settings.id,
      set: { mobileEnabled: true },
    });

  return {
    ownerId: user.id,
    serverId: server.id,
    serverUserId: serverUser.id,
  };
}

// Clean up test data
async function cleanupTestData(): Promise<void> {
  // Clean in reverse order of dependencies
  await db.delete(mobileSessions);
  await db.delete(mobileTokens);
  await db.delete(serverUsers);
  await db.delete(servers);
  await db.delete(users);
}

// Generate owner JWT token
function generateOwnerToken(app: FastifyInstance, testData: TestData): string {
  return app.jwt.sign(
    {
      userId: testData.ownerId,
      username: 'testowner',
      role: 'owner',
      serverIds: [testData.serverId],
    } as AuthUser,
    { expiresIn: '1h' }
  );
}

describe('Mobile Authentication Integration Tests', () => {
  let app: FastifyInstance;
  let testData: TestData;

  beforeAll(async () => {
    app = await createMobileTestApp();
    await app.ready();
  });

  afterAll(async () => {
    await app.close();
  });

  beforeEach(async () => {
    await cleanupTestData();
    testData = await seedTestData();
    // Reset mock Redis
    (app.redis as any)._reset();
  });

  describe('POST /api/v1/mobile/pair-token - Generate Pairing Token', () => {
    it('should generate a valid pairing token for owner', async () => {
      const ownerToken = generateOwnerToken(app, testData);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/pair-token',
        headers: { Authorization: `Bearer ${ownerToken}` },
      });

      expect(res.statusCode).toBe(200);
      const body = res.json();
      expect(body.token).toBeDefined();
      expect(body.token).toMatch(/^trr_mob_/);
      expect(body.expiresAt).toBeDefined();

      // Verify token was stored in database
      const tokenHash = hashToken(body.token);
      const [storedToken] = await db
        .select()
        .from(mobileTokens)
        .where(eq(mobileTokens.tokenHash, tokenHash));

      expect(storedToken).toBeDefined();
      expect(storedToken.createdBy).toBe(testData.ownerId);
      expect(storedToken.usedAt).toBeNull();
    });

    it('should reject non-owner users', async () => {
      // Create a viewer token
      const viewerToken = app.jwt.sign(
        {
          userId: testData.ownerId,
          username: 'testviewer',
          role: 'viewer',
          serverIds: [],
        } as AuthUser,
        { expiresIn: '1h' }
      );

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/pair-token',
        headers: { Authorization: `Bearer ${viewerToken}` },
      });

      expect(res.statusCode).toBe(403);
    });

    it('should reject unauthenticated requests', async () => {
      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/pair-token',
      });

      expect(res.statusCode).toBe(401);
    });

    it('should reject when mobile is disabled', async () => {
      // Disable mobile
      await db.update(settings).set({ mobileEnabled: false }).where(eq(settings.id, 1));

      const ownerToken = generateOwnerToken(app, testData);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/pair-token',
        headers: { Authorization: `Bearer ${ownerToken}` },
      });

      expect(res.statusCode).toBe(400);
      expect(res.json().message).toContain('Mobile access is not enabled');
    });

    it('should not count expired tokens toward pending limit', async () => {
      const ownerToken = generateOwnerToken(app, testData);

      // Insert an expired token - should not count toward limit
      await db.insert(mobileTokens).values({
        tokenHash: 'expired-token-hash-1234567890abcdef1234567890abcdef',
        expiresAt: new Date(Date.now() - 1000 * 60 * 60), // 1 hour ago
        createdBy: testData.ownerId,
      });

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/pair-token',
        headers: { Authorization: `Bearer ${ownerToken}` },
      });

      expect(res.statusCode).toBe(200);

      // Both tokens exist (expired tokens are not automatically cleaned up)
      const tokens = await db.select().from(mobileTokens);
      expect(tokens.length).toBe(2);
    });

    it('should enforce max pending tokens limit', async () => {
      const ownerToken = generateOwnerToken(app, testData);

      // Create 3 pending tokens (MAX_PENDING_TOKENS)
      for (let i = 0; i < 3; i++) {
        await db.insert(mobileTokens).values({
          tokenHash: `pending-token-hash-${i}-abcdef1234567890abcdef1234567890`,
          expiresAt: new Date(Date.now() + 1000 * 60 * 15), // 15 min from now
          createdBy: testData.ownerId,
        });
      }

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/pair-token',
        headers: { Authorization: `Bearer ${ownerToken}` },
      });

      expect(res.statusCode).toBe(400);
      expect(res.json().message).toContain('Maximum of 3 pending tokens');
    });
  });

  describe('POST /api/v1/mobile/pair - Exchange Token for JWT', () => {
    let validPairingToken: string;

    beforeEach(async () => {
      // Generate a valid pairing token in the database
      validPairingToken = generateTestMobileToken();
      await db.insert(mobileTokens).values({
        tokenHash: hashToken(validPairingToken),
        expiresAt: new Date(Date.now() + 1000 * 60 * TOKEN_EXPIRY_MINUTES),
        createdBy: testData.ownerId,
      });
    });

    it('should exchange valid token for JWT and refresh token', async () => {
      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/pair',
        payload: {
          token: validPairingToken,
          deviceName: 'Test iPhone',
          deviceId: 'device-12345',
          platform: 'ios',
        },
      });

      expect(res.statusCode).toBe(200);
      const body = res.json();
      expect(body.accessToken).toBeDefined();
      expect(body.refreshToken).toBeDefined();
      expect(body.server).toBeDefined();
      expect(body.user).toBeDefined();
      expect(body.user.username).toBe('testowner');

      // Verify mobile session was created
      const [session] = await db
        .select()
        .from(mobileSessions)
        .where(eq(mobileSessions.deviceId, 'device-12345'));

      expect(session).toBeDefined();
      expect(session.deviceName).toBe('Test iPhone');
      expect(session.platform).toBe('ios');

      // Verify pairing token was marked as used
      const [usedToken] = await db
        .select()
        .from(mobileTokens)
        .where(eq(mobileTokens.tokenHash, hashToken(validPairingToken)));

      expect(usedToken.usedAt).not.toBeNull();
    });

    it('should reject expired pairing token', async () => {
      // Create an expired token
      const expiredToken = generateTestMobileToken();
      await db.insert(mobileTokens).values({
        tokenHash: hashToken(expiredToken),
        expiresAt: new Date(Date.now() - 1000 * 60), // 1 minute ago
        createdBy: testData.ownerId,
      });

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/pair',
        payload: {
          token: expiredToken,
          deviceName: 'Test iPhone',
          deviceId: 'device-12345',
          platform: 'ios',
        },
      });

      expect(res.statusCode).toBe(401);
      expect(res.json().message).toContain('expired');
    });

    it('should reject already-used pairing token', async () => {
      // First, use the token
      await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/pair',
        payload: {
          token: validPairingToken,
          deviceName: 'First Device',
          deviceId: 'device-first',
          platform: 'ios',
        },
      });

      // Try to use it again
      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/pair',
        payload: {
          token: validPairingToken,
          deviceName: 'Second Device',
          deviceId: 'device-second',
          platform: 'android',
        },
      });

      expect(res.statusCode).toBe(401);
      expect(res.json().message).toContain('already been used');
    });

    it('should reject invalid token format', async () => {
      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/pair',
        payload: {
          token: 'invalid-token',
          deviceName: 'Test iPhone',
          deviceId: 'device-12345',
          platform: 'ios',
        },
      });

      // Token without correct prefix returns 401 unauthorized
      expect(res.statusCode).toBe(401);
    });

    it('should allow pairing even when mobile is disabled (token was pre-generated)', async () => {
      // Note: /pair doesn't check mobileEnabled - tokens can be used if they were
      // generated before mobile was disabled. Disabling mobile only prevents NEW tokens.
      await db.update(settings).set({ mobileEnabled: false }).where(eq(settings.id, 1));

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/pair',
        payload: {
          token: validPairingToken,
          deviceName: 'Test iPhone',
          deviceId: 'device-12345',
          platform: 'ios',
        },
      });

      // Pairing succeeds because the token was valid
      expect(res.statusCode).toBe(200);
      expect(res.json().accessToken).toBeDefined();
    });

    it('should enforce max paired devices limit', async () => {
      // Create 5 existing sessions (MAX_PAIRED_DEVICES)
      for (let i = 0; i < 5; i++) {
        await db.insert(mobileSessions).values({
          userId: testData.ownerId,
          refreshTokenHash: `existing-refresh-hash-${i}-abcdef1234567890abc`,
          deviceName: `Existing Device ${i}`,
          deviceId: `existing-device-${i}`,
          platform: 'ios',
        });
      }

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/pair',
        payload: {
          token: validPairingToken,
          deviceName: 'New Device',
          deviceId: 'device-new',
          platform: 'android',
        },
      });

      expect(res.statusCode).toBe(400);
      expect(res.json().message).toContain('Maximum of 5 devices');
    });

    it('should accept device secret for push encryption', async () => {
      const deviceSecret = randomBytes(32).toString('base64');

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/pair',
        payload: {
          token: validPairingToken,
          deviceName: 'Test iPhone',
          deviceId: 'device-12345',
          platform: 'ios',
          deviceSecret,
        },
      });

      expect(res.statusCode).toBe(200);

      // Verify device secret was stored
      const [session] = await db
        .select()
        .from(mobileSessions)
        .where(eq(mobileSessions.deviceId, 'device-12345'));

      expect(session.deviceSecret).toBe(deviceSecret);
    });
  });

  describe('POST /api/v1/mobile/refresh - Refresh JWT Token', () => {
    let validRefreshToken: string;
    let mobileJwt: string;

    beforeEach(async () => {
      // Generate a pairing token and pair a device
      const pairingToken = generateTestMobileToken();
      await db.insert(mobileTokens).values({
        tokenHash: hashToken(pairingToken),
        expiresAt: new Date(Date.now() + 1000 * 60 * TOKEN_EXPIRY_MINUTES),
        createdBy: testData.ownerId,
      });

      const pairRes = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/pair',
        payload: {
          token: pairingToken,
          deviceName: 'Test Device',
          deviceId: 'device-refresh-test',
          platform: 'ios',
        },
      });

      const pairBody = pairRes.json();
      validRefreshToken = pairBody.refreshToken;
      mobileJwt = pairBody.accessToken;
    });

    it('should refresh token and rotate refresh token', async () => {
      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/refresh',
        payload: {
          refreshToken: validRefreshToken,
        },
      });

      expect(res.statusCode).toBe(200);
      const body = res.json();
      expect(body.accessToken).toBeDefined();
      expect(body.refreshToken).toBeDefined();

      // New refresh token should be different (rotation)
      expect(body.refreshToken).not.toBe(validRefreshToken);

      // Old refresh token should no longer work
      const secondRes = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/refresh',
        payload: {
          refreshToken: validRefreshToken,
        },
      });

      expect(secondRes.statusCode).toBe(401);
    });

    it('should reject invalid refresh token', async () => {
      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/refresh',
        payload: {
          refreshToken: 'invalid-refresh-token',
        },
      });

      expect(res.statusCode).toBe(401);
      expect(res.json().message).toContain('Invalid or expired refresh token');
    });

    it('should allow refresh even when mobile is disabled', async () => {
      // Note: /refresh doesn't check mobileEnabled - existing sessions continue working.
      // Disabling mobile only prevents NEW tokens and revokes sessions when explicitly disabled.
      await db.update(settings).set({ mobileEnabled: false }).where(eq(settings.id, 1));

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/refresh',
        payload: {
          refreshToken: validRefreshToken,
        },
      });

      // Refresh succeeds because the session is still valid
      expect(res.statusCode).toBe(200);
      expect(res.json().accessToken).toBeDefined();
    });

    it('should update lastSeenAt on refresh', async () => {
      // Get the session before refresh
      const [sessionBefore] = await db
        .select()
        .from(mobileSessions)
        .where(eq(mobileSessions.deviceId, 'device-refresh-test'));

      const lastSeenBefore = sessionBefore.lastSeenAt;

      // Wait a bit to ensure timestamp difference
      await new Promise((resolve) => setTimeout(resolve, 100));

      await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/refresh',
        payload: {
          refreshToken: validRefreshToken,
        },
      });

      // Get the session after refresh
      const [sessionAfter] = await db
        .select()
        .from(mobileSessions)
        .where(eq(mobileSessions.deviceId, 'device-refresh-test'));

      expect(sessionAfter.lastSeenAt.getTime()).toBeGreaterThan(lastSeenBefore.getTime());
    });
  });

  describe('DELETE /api/v1/mobile/sessions/:id - Revoke Session', () => {
    let sessionId: string;

    beforeEach(async () => {
      // Create a mobile session
      const [session] = await db
        .insert(mobileSessions)
        .values({
          userId: testData.ownerId,
          refreshTokenHash: 'test-refresh-hash-for-deletion-1234567890abcdef',
          deviceName: 'Device to Delete',
          deviceId: 'device-to-delete',
          platform: 'ios',
        })
        .returning();

      sessionId = session.id;
    });

    it('should revoke session as owner', async () => {
      const ownerToken = generateOwnerToken(app, testData);

      const res = await app.inject({
        method: 'DELETE',
        url: `/api/v1/mobile/sessions/${sessionId}`,
        headers: { Authorization: `Bearer ${ownerToken}` },
      });

      expect(res.statusCode).toBe(200);
      expect(res.json().success).toBe(true);

      // Verify session was deleted
      const sessions = await db
        .select()
        .from(mobileSessions)
        .where(eq(mobileSessions.id, sessionId));

      expect(sessions.length).toBe(0);
    });

    it('should reject non-owner users', async () => {
      const viewerToken = app.jwt.sign(
        {
          userId: testData.ownerId,
          username: 'testviewer',
          role: 'viewer',
          serverIds: [],
        } as AuthUser,
        { expiresIn: '1h' }
      );

      const res = await app.inject({
        method: 'DELETE',
        url: `/api/v1/mobile/sessions/${sessionId}`,
        headers: { Authorization: `Bearer ${viewerToken}` },
      });

      expect(res.statusCode).toBe(403);
    });

    it('should return 404 for non-existent session', async () => {
      const ownerToken = generateOwnerToken(app, testData);

      const res = await app.inject({
        method: 'DELETE',
        url: '/api/v1/mobile/sessions/00000000-0000-0000-0000-000000000000',
        headers: { Authorization: `Bearer ${ownerToken}` },
      });

      expect(res.statusCode).toBe(404);
    });

    it('should reject unauthenticated requests', async () => {
      const res = await app.inject({
        method: 'DELETE',
        url: `/api/v1/mobile/sessions/${sessionId}`,
      });

      expect(res.statusCode).toBe(401);
    });
  });

  describe('DELETE /api/v1/mobile/sessions - Revoke All Sessions', () => {
    beforeEach(async () => {
      // Create multiple mobile sessions
      for (let i = 0; i < 3; i++) {
        await db.insert(mobileSessions).values({
          userId: testData.ownerId,
          refreshTokenHash: `bulk-delete-refresh-hash-${i}-abcdef1234567890`,
          deviceName: `Device ${i}`,
          deviceId: `device-bulk-${i}`,
          platform: i % 2 === 0 ? 'ios' : 'android',
        });
      }
    });

    it('should revoke all sessions as owner', async () => {
      const ownerToken = generateOwnerToken(app, testData);

      const res = await app.inject({
        method: 'DELETE',
        url: '/api/v1/mobile/sessions',
        headers: { Authorization: `Bearer ${ownerToken}` },
      });

      expect(res.statusCode).toBe(200);
      const body = res.json();
      expect(body.success).toBe(true);
      expect(body.revokedCount).toBe(3);

      // Verify all sessions were deleted
      const sessions = await db.select().from(mobileSessions);
      expect(sessions.length).toBe(0);
    });

    it('should return success even with no sessions to revoke', async () => {
      // Clean up all sessions first
      await db.delete(mobileSessions);

      const ownerToken = generateOwnerToken(app, testData);

      const res = await app.inject({
        method: 'DELETE',
        url: '/api/v1/mobile/sessions',
        headers: { Authorization: `Bearer ${ownerToken}` },
      });

      expect(res.statusCode).toBe(200);
      expect(res.json().revokedCount).toBe(0);
    });

    it('should reject non-owner users', async () => {
      const viewerToken = app.jwt.sign(
        {
          userId: testData.ownerId,
          username: 'testviewer',
          role: 'viewer',
          serverIds: [],
        } as AuthUser,
        { expiresIn: '1h' }
      );

      const res = await app.inject({
        method: 'DELETE',
        url: '/api/v1/mobile/sessions',
        headers: { Authorization: `Bearer ${viewerToken}` },
      });

      expect(res.statusCode).toBe(403);
    });
  });

  describe('GET /api/v1/mobile - Get Mobile Config', () => {
    it('should return mobile config for owner', async () => {
      // Create a session to verify it appears in config
      await db.insert(mobileSessions).values({
        userId: testData.ownerId,
        refreshTokenHash: 'config-test-refresh-hash-abcdef1234567890abcd',
        deviceName: 'Config Test Device',
        deviceId: 'device-config-test',
        platform: 'ios',
        expoPushToken: 'ExponentPushToken[test123]',
      });

      const ownerToken = generateOwnerToken(app, testData);

      const res = await app.inject({
        method: 'GET',
        url: '/api/v1/mobile',
        headers: { Authorization: `Bearer ${ownerToken}` },
      });

      expect(res.statusCode).toBe(200);
      const body = res.json();
      expect(body.isEnabled).toBe(true);
      expect(body.sessions).toBeDefined();
      expect(body.sessions.length).toBe(1);
      expect(body.sessions[0].deviceName).toBe('Config Test Device');
      expect(body.sessions[0].platform).toBe('ios');
      expect(body.serverName).toBeDefined();
      expect(body.maxDevices).toBe(5);
    });

    it('should reject non-owner users', async () => {
      const viewerToken = app.jwt.sign(
        {
          userId: testData.ownerId,
          username: 'testviewer',
          role: 'viewer',
          serverIds: [],
        } as AuthUser,
        { expiresIn: '1h' }
      );

      const res = await app.inject({
        method: 'GET',
        url: '/api/v1/mobile',
        headers: { Authorization: `Bearer ${viewerToken}` },
      });

      expect(res.statusCode).toBe(403);
    });
  });

  describe('POST /api/v1/mobile/enable - Enable Mobile Access', () => {
    beforeEach(async () => {
      // Ensure mobile is disabled
      await db.update(settings).set({ mobileEnabled: false }).where(eq(settings.id, 1));
    });

    it('should enable mobile access as owner', async () => {
      const ownerToken = generateOwnerToken(app, testData);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/enable',
        headers: { Authorization: `Bearer ${ownerToken}` },
      });

      expect(res.statusCode).toBe(200);
      expect(res.json().isEnabled).toBe(true);

      // Verify in database
      const [settingsRow] = await db.select().from(settings).where(eq(settings.id, 1));
      expect(settingsRow.mobileEnabled).toBe(true);
    });

    it('should reject non-owner users', async () => {
      const viewerToken = app.jwt.sign(
        {
          userId: testData.ownerId,
          username: 'testviewer',
          role: 'viewer',
          serverIds: [],
        } as AuthUser,
        { expiresIn: '1h' }
      );

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/enable',
        headers: { Authorization: `Bearer ${viewerToken}` },
      });

      expect(res.statusCode).toBe(403);
    });
  });

  describe('POST /api/v1/mobile/disable - Disable Mobile Access', () => {
    it('should disable mobile access and revoke all sessions', async () => {
      // Create some sessions
      for (let i = 0; i < 2; i++) {
        await db.insert(mobileSessions).values({
          userId: testData.ownerId,
          refreshTokenHash: `disable-test-refresh-hash-${i}-abcdef1234567`,
          deviceName: `Device ${i}`,
          deviceId: `device-disable-${i}`,
          platform: 'ios',
        });
      }

      const ownerToken = generateOwnerToken(app, testData);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/disable',
        headers: { Authorization: `Bearer ${ownerToken}` },
      });

      expect(res.statusCode).toBe(200);
      expect(res.json().success).toBe(true);

      // Verify mobile is disabled
      const [settingsRow] = await db.select().from(settings).where(eq(settings.id, 1));
      expect(settingsRow.mobileEnabled).toBe(false);

      // Verify all sessions were revoked
      const sessions = await db.select().from(mobileSessions);
      expect(sessions.length).toBe(0);
    });

    it('should reject non-owner users', async () => {
      const viewerToken = app.jwt.sign(
        {
          userId: testData.ownerId,
          username: 'testviewer',
          role: 'viewer',
          serverIds: [],
        } as AuthUser,
        { expiresIn: '1h' }
      );

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/mobile/disable',
        headers: { Authorization: `Bearer ${viewerToken}` },
      });

      expect(res.statusCode).toBe(403);
    });
  });
});
