import Redis from 'ioredis';
import { test, expect, createTestUser, registerUser } from '../fixtures';

/**
 * E2E coverage for PATCH /auth/profile OTP-gated email/phone changes.
 *
 * Flow: PATCH /auth/profile → OTP stored in Redis (via notifications bus) →
 * POST /auth/profile/verify-email|verify-phone → persisted user state.
 *
 * We read the OTP code directly from Redis because the notifications transport
 * is asynchronous in dev/test. This is acceptable for an e2e that is already
 * exercising the same infra the API uses.
 */

const EMAIL_OTP_PREFIX = 'auth:email_change_otp';
const PHONE_OTP_PREFIX = 'auth:phone_change_otp';

function redisClient(): Redis {
  return new Redis({
    host: process.env.REDIS_HOST ?? 'localhost',
    port: Number(process.env.REDIS_PORT ?? 6379),
    lazyConnect: true,
    maxRetriesPerRequest: 1,
  });
}

async function readOtp(userId: string, prefix: string): Promise<string | null> {
  const redis = redisClient();
  try {
    await redis.connect();
    const raw = await redis.get(`${prefix}:${userId}`);
    if (!raw) return null;
    const parsed = JSON.parse(raw) as { code: string };
    return parsed.code;
  } catch {
    return null;
  } finally {
    await redis.quit().catch(() => undefined);
  }
}

test.describe('PATCH /auth/profile — OTP-gated email change', () => {
  test('request → OTP → confirm → persisted', async ({ request, authedRequest, testTokens }) => {
    // Decode JWT to get userId without a DB round-trip.
    const payload = JSON.parse(
      Buffer.from(testTokens.accessToken.split('.')[1] ?? '', 'base64url').toString('utf8'),
    ) as { sub: string };
    const userId = payload.sub;
    const newEmail = `updated${Date.now()}@goodgo.test`;

    const patchRes = await authedRequest.patch('auth/profile', { data: { email: newEmail } });
    expect(patchRes.status()).toBe(200);
    const patchBody = await patchRes.json();
    expect(patchBody.data.emailChangePending).toBe(true);
    // Email should NOT be persisted yet.
    expect(patchBody.data.email).not.toBe(newEmail);

    const code = await readOtp(userId, EMAIL_OTP_PREFIX);
    expect(code, 'OTP code should be stored in Redis').toMatch(/^\d{6}$/);

    // Wrong code is rejected.
    const badRes = await authedRequest.post('auth/profile/verify-email', {
      data: { code: '000000' },
    });
    expect([400, 422]).toContain(badRes.status());

    // Correct code commits the change.
    const okRes = await authedRequest.post('auth/profile/verify-email', {
      data: { code: code! },
    });
    expect(okRes.status()).toBe(201);
    const okBody = await okRes.json();
    expect(okBody.data.email).toBe(newEmail);

    // GET /auth/profile now shows the new email.
    const profileRes = await authedRequest.get('auth/profile');
    expect(profileRes.status()).toBe(200);
    const profile = await profileRes.json();
    expect(profile.email).toBe(newEmail);

    // OTP is consumed — replaying fails.
    const replayRes = await authedRequest.post('auth/profile/verify-email', {
      data: { code: code! },
    });
    expect([400, 422]).toContain(replayRes.status());

    // Unauthenticated request is rejected.
    const unauthRes = await request.post('auth/profile/verify-email', { data: { code: '123456' } });
    expect(unauthRes.status()).toBe(401);
  });

  test('expired / missing OTP returns validation error', async ({ authedRequest }) => {
    const res = await authedRequest.post('auth/profile/verify-email', {
      data: { code: '123456' },
    });
    expect([400, 422]).toContain(res.status());
  });
});

test.describe('PATCH /auth/profile — OTP-gated phone change', () => {
  test('request → OTP → confirm → persisted', async ({ request }) => {
    // Fresh user so we can change phone without colliding with fixtures.
    const user = createTestUser();
    const { accessToken } = await registerUser(request, user);
    const payload = JSON.parse(
      Buffer.from(accessToken.split('.')[1] ?? '', 'base64url').toString('utf8'),
    ) as { sub: string };
    const userId = payload.sub;

    const headers = { Authorization: `Bearer ${accessToken}` };
    const newPhone = `09${Date.now().toString().slice(-8)}`;

    const patchRes = await request.patch('auth/profile', {
      headers,
      data: { phoneNumber: newPhone },
    });
    expect(patchRes.status()).toBe(200);
    const patchBody = await patchRes.json();
    expect(patchBody.data.phoneChangePending).toBe(true);

    const code = await readOtp(userId, PHONE_OTP_PREFIX);
    expect(code, 'SMS OTP code should be stored in Redis').toMatch(/^\d{6}$/);

    const okRes = await request.post('auth/profile/verify-phone', {
      headers,
      data: { code: code! },
    });
    expect(okRes.status()).toBe(201);
    const okBody = await okRes.json();
    // Phone is normalised server-side (+84...)
    expect(okBody.data.phoneNumber).toContain(newPhone.slice(1));
  });
});
