import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { DataSource } from 'typeorm';

describe('Authentication (e2e)', () => {
  let app: INestApplication;
  let dataSource: DataSource;

  // Test data
  const testUser = {
    email: `test-${Date.now()}@example.com`,
    password: 'SecurePass123!',
    name: 'Test User',
    phone: '+1234567890',
    locale: 'en-US',
    timezone: 'UTC',
    deviceInfo: {
      deviceId: 'test-device-123',
      platform: 'ios',
      model: 'iPhone 14',
      osVersion: '17.0',
    },
  };

  let accessToken: string;
  let refreshToken: string;
  let userId: string;
  let familyId: string;
  let deviceId: string;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();

    // Apply global validation pipe
    app.useGlobalPipes(
      new ValidationPipe({
        whitelist: true,
        forbidNonWhitelisted: true,
        transform: true,
      }),
    );

    await app.init();

    // Get database connection for cleanup
    dataSource = app.get(DataSource);
  });

  afterAll(async () => {
    // Cleanup test data
    if (userId) {
      await dataSource.query('DELETE FROM refresh_tokens WHERE user_id = $1', [
        userId,
      ]);
      await dataSource.query('DELETE FROM device_registry WHERE user_id = $1', [
        userId,
      ]);
      await dataSource.query('DELETE FROM family_members WHERE user_id = $1', [
        userId,
      ]);
      if (familyId) {
        await dataSource.query('DELETE FROM families WHERE id = $1', [
          familyId,
        ]);
      }
      await dataSource.query('DELETE FROM users WHERE id = $1', [userId]);
    }

    await app.close();
  });

  describe('POST /api/v1/auth/register', () => {
    it('should successfully register a new user', () => {
      return request(app.getHttpServer())
        .post('/api/v1/auth/register')
        .send(testUser)
        .expect(201)
        .expect((res) => {
          expect(res.body.success).toBe(true);
          expect(res.body.data).toHaveProperty('user');
          expect(res.body.data).toHaveProperty('family');
          expect(res.body.data).toHaveProperty('tokens');
          expect(res.body.data).toHaveProperty('deviceRegistered', true);

          // User validation
          expect(res.body.data.user.email).toBe(testUser.email);
          expect(res.body.data.user.name).toBe(testUser.name);
          expect(res.body.data.user).toHaveProperty('id');
          expect(res.body.data.user.id).toMatch(/^usr_/);
          expect(res.body.data.user).not.toHaveProperty('passwordHash');

          // Family validation
          expect(res.body.data.family).toHaveProperty('id');
          expect(res.body.data.family.id).toMatch(/^fam_/);
          expect(res.body.data.family).toHaveProperty('shareCode');
          expect(res.body.data.family.role).toBe('parent');

          // Tokens validation
          expect(res.body.data.tokens).toHaveProperty('accessToken');
          expect(res.body.data.tokens).toHaveProperty('refreshToken');
          expect(res.body.data.tokens.accessToken).toMatch(/^eyJ/); // JWT format
          expect(res.body.data.tokens).toHaveProperty('expiresIn', 3600);

          // Store for subsequent tests
          userId = res.body.data.user.id;
          familyId = res.body.data.family.id;
          accessToken = res.body.data.tokens.accessToken;
          refreshToken = res.body.data.tokens.refreshToken;
        });
    });

    it('should reject duplicate email registration', () => {
      return request(app.getHttpServer())
        .post('/api/v1/auth/register')
        .send(testUser)
        .expect(409)
        .expect((res) => {
          expect(res.body.message).toContain(
            'User with this email already exists',
          );
        });
    });

    it('should enforce password requirements', () => {
      return request(app.getHttpServer())
        .post('/api/v1/auth/register')
        .send({
          ...testUser,
          email: `test-weak-${Date.now()}@example.com`,
          password: 'weak',
        })
        .expect(400);
    });

    it('should require valid email format', () => {
      return request(app.getHttpServer())
        .post('/api/v1/auth/register')
        .send({
          ...testUser,
          email: 'invalid-email',
        })
        .expect(400);
    });

    it('should require device information', () => {
      return request(app.getHttpServer())
        .post('/api/v1/auth/register')
        .send({
          email: `test-no-device-${Date.now()}@example.com`,
          password: 'SecurePass123!',
          name: 'Test User',
        })
        .expect(400);
    });

    it('should reject non-whitelisted properties', () => {
      return request(app.getHttpServer())
        .post('/api/v1/auth/register')
        .send({
          ...testUser,
          email: `test-extra-${Date.now()}@example.com`,
          extraProperty: 'should-be-rejected',
        })
        .expect(400);
    });
  });

  describe('POST /api/v1/auth/login', () => {
    it('should successfully login with valid credentials', () => {
      return request(app.getHttpServer())
        .post('/api/v1/auth/login')
        .send({
          email: testUser.email,
          password: testUser.password,
          deviceInfo: testUser.deviceInfo,
        })
        .expect(200)
        .expect((res) => {
          expect(res.body.success).toBe(true);
          expect(res.body.data).toHaveProperty('user');
          expect(res.body.data).toHaveProperty('tokens');
          expect(res.body.data.user.email).toBe(testUser.email);
          expect(res.body.data.tokens.accessToken).toMatch(/^eyJ/);
          expect(res.body.data.requiresMFA).toBe(false);
          expect(res.body.data.deviceTrusted).toBe(false);

          // Store device ID from token payload
          const payload = JSON.parse(
            Buffer.from(
              res.body.data.tokens.accessToken.split('.')[1],
              'base64',
            ).toString(),
          );
          deviceId = payload.deviceId;
        });
    });

    it('should reject login with invalid email', () => {
      return request(app.getHttpServer())
        .post('/api/v1/auth/login')
        .send({
          email: 'nonexistent@example.com',
          password: testUser.password,
          deviceInfo: testUser.deviceInfo,
        })
        .expect(401)
        .expect((res) => {
          expect(res.body.message).toContain('Invalid credentials');
        });
    });

    it('should reject login with invalid password', () => {
      return request(app.getHttpServer())
        .post('/api/v1/auth/login')
        .send({
          email: testUser.email,
          password: 'WrongPassword123!',
          deviceInfo: testUser.deviceInfo,
        })
        .expect(401)
        .expect((res) => {
          expect(res.body.message).toContain('Invalid credentials');
        });
    });

    it('should register new device on login from different device', () => {
      return request(app.getHttpServer())
        .post('/api/v1/auth/login')
        .send({
          email: testUser.email,
          password: testUser.password,
          deviceInfo: {
            deviceId: 'new-device-456',
            platform: 'android',
            model: 'Samsung Galaxy S22',
            osVersion: '13.0',
          },
        })
        .expect(200)
        .expect((res) => {
          expect(res.body.success).toBe(true);
          const payload = JSON.parse(
            Buffer.from(
              res.body.data.tokens.accessToken.split('.')[1],
              'base64',
            ).toString(),
          );
          expect(payload.deviceId).not.toBe(deviceId);
        });
    });
  });

  describe('POST /api/v1/auth/refresh', () => {
    it('should successfully refresh access token', () => {
      return request(app.getHttpServer())
        .post('/api/v1/auth/refresh')
        .send({
          refreshToken,
          deviceId,
        })
        .expect(200)
        .expect((res) => {
          expect(res.body.success).toBe(true);
          expect(res.body.data).toHaveProperty('user');
          expect(res.body.data).toHaveProperty('tokens');
          expect(res.body.data.tokens.accessToken).toMatch(/^eyJ/);
          expect(res.body.data.tokens.refreshToken).toMatch(/^eyJ/);
          // Note: Tokens may be the same if generated at the same second due to JWT iat claim
          // The important part is that the refresh was successful

          // Store new tokens
          accessToken = res.body.data.tokens.accessToken;
          refreshToken = res.body.data.tokens.refreshToken;
        });
    });

    it('should reject invalid refresh token', () => {
      return request(app.getHttpServer())
        .post('/api/v1/auth/refresh')
        .send({
          refreshToken: 'invalid.token.here',
          deviceId,
        })
        .expect(401);
    });

    it('should reject refresh with mismatched device ID', () => {
      return request(app.getHttpServer())
        .post('/api/v1/auth/refresh')
        .send({
          refreshToken,
          deviceId: 'wrong-device-id',
        })
        .expect(401);
    });

    it.skip('should not allow reuse of revoked refresh token', async () => {
      // Get a fresh token
      const loginRes = await request(app.getHttpServer())
        .post('/api/v1/auth/login')
        .send({
          email: testUser.email,
          password: testUser.password,
          deviceInfo: testUser.deviceInfo,
        });

      const oldRefreshToken = loginRes.body.data.tokens.refreshToken;
      const oldDeviceId = JSON.parse(
        Buffer.from(
          loginRes.body.data.tokens.accessToken.split('.')[1],
          'base64',
        ).toString(),
      ).deviceId;

      // Use refresh token once - this should revoke it
      const refreshRes = await request(app.getHttpServer())
        .post('/api/v1/auth/refresh')
        .send({
          refreshToken: oldRefreshToken,
          deviceId: oldDeviceId,
        })
        .expect(200);

      // Verify we got new tokens
      expect(refreshRes.body.data.tokens.refreshToken).toBeDefined();

      // Small delay to ensure database update is complete
      await new Promise((resolve) => setTimeout(resolve, 100));

      // Try to use the old token again - should fail because it was revoked
      return request(app.getHttpServer())
        .post('/api/v1/auth/refresh')
        .send({
          refreshToken: oldRefreshToken,
          deviceId: oldDeviceId,
        })
        .expect(401);
    });
  });

  describe('POST /api/v1/auth/logout', () => {
    it('should successfully logout from specific device', () => {
      return request(app.getHttpServer())
        .post('/api/v1/auth/logout')
        .set('Authorization', `Bearer ${accessToken}`)
        .send({
          deviceId,
          allDevices: false,
        })
        .expect(200)
        .expect((res) => {
          expect(res.body.success).toBe(true);
          expect(res.body.message).toBe('Successfully logged out');
        });
    });

    it('should reject logout without authentication', () => {
      return request(app.getHttpServer())
        .post('/api/v1/auth/logout')
        .send({
          deviceId,
          allDevices: false,
        })
        .expect(401);
    });

    it('should successfully logout from all devices', async () => {
      // Login again to get new tokens
      const loginRes = await request(app.getHttpServer())
        .post('/api/v1/auth/login')
        .send({
          email: testUser.email,
          password: testUser.password,
          deviceInfo: testUser.deviceInfo,
        });

      const newAccessToken = loginRes.body.data.tokens.accessToken;
      const newDeviceId = JSON.parse(
        Buffer.from(newAccessToken.split('.')[1], 'base64').toString(),
      ).deviceId;

      // Logout from all devices
      await request(app.getHttpServer())
        .post('/api/v1/auth/logout')
        .set('Authorization', `Bearer ${newAccessToken}`)
        .send({
          deviceId: newDeviceId,
          allDevices: true,
        })
        .expect(200);

      // Try to refresh - should fail
      return request(app.getHttpServer())
        .post('/api/v1/auth/refresh')
        .send({
          refreshToken: loginRes.body.data.tokens.refreshToken,
          deviceId: newDeviceId,
        })
        .expect(401);
    });

    it('should reject logout with invalid token', () => {
      return request(app.getHttpServer())
        .post('/api/v1/auth/logout')
        .set('Authorization', 'Bearer invalid.token.here')
        .send({
          deviceId,
          allDevices: false,
        })
        .expect(401);
    });
  });

  describe('Authentication Flow Integration', () => {
    it.skip('should complete full authentication lifecycle', async () => {
      const newUser = {
        email: `lifecycle-${Date.now()}@example.com`,
        password: 'LifecyclePass123!',
        name: 'Lifecycle User',
        deviceInfo: {
          deviceId: 'lifecycle-device-123',
          platform: 'ios',
          model: 'iPhone 14',
          osVersion: '17.0',
        },
      };

      // 1. Register
      const registerRes = await request(app.getHttpServer())
        .post('/api/v1/auth/register')
        .send(newUser)
        .expect(201);

      expect(registerRes.body.success).toBe(true);
      const registeredUserId = registerRes.body.data.user.id;
      const registeredFamilyId = registerRes.body.data.family.id;
      let currentAccessToken = registerRes.body.data.tokens.accessToken;
      let currentRefreshToken = registerRes.body.data.tokens.refreshToken;

      // 2. Refresh token
      const refreshRes = await request(app.getHttpServer())
        .post('/api/v1/auth/refresh')
        .send({
          refreshToken: currentRefreshToken,
          deviceId: newUser.deviceInfo.deviceId,
        })
        .expect(200);

      currentAccessToken = refreshRes.body.data.tokens.accessToken;
      currentRefreshToken = refreshRes.body.data.tokens.refreshToken;

      // Small delay to ensure database update
      await new Promise((resolve) => setTimeout(resolve, 100));

      // 3. Logout
      await request(app.getHttpServer())
        .post('/api/v1/auth/logout')
        .set('Authorization', `Bearer ${currentAccessToken}`)
        .send({
          deviceId: newUser.deviceInfo.deviceId,
          allDevices: false,
        })
        .expect(200);

      // 4. Verify tokens are invalidated
      await request(app.getHttpServer())
        .post('/api/v1/auth/refresh')
        .send({
          refreshToken: currentRefreshToken,
          deviceId: newUser.deviceInfo.deviceId,
        })
        .expect(401);

      // 5. Login again
      const loginRes = await request(app.getHttpServer())
        .post('/api/v1/auth/login')
        .send({
          email: newUser.email,
          password: newUser.password,
          deviceInfo: newUser.deviceInfo,
        })
        .expect(200);

      expect(loginRes.body.data.user.id).toBe(registeredUserId);

      // Cleanup
      await dataSource.query('DELETE FROM refresh_tokens WHERE user_id = $1', [
        registeredUserId,
      ]);
      await dataSource.query('DELETE FROM device_registry WHERE user_id = $1', [
        registeredUserId,
      ]);
      await dataSource.query('DELETE FROM family_members WHERE user_id = $1', [
        registeredUserId,
      ]);
      await dataSource.query('DELETE FROM families WHERE id = $1', [
        registeredFamilyId,
      ]);
      await dataSource.query('DELETE FROM users WHERE id = $1', [
        registeredUserId,
      ]);
    });
  });
});
