import { ConfigService } from '@nestjs/config';
import { exportJWK, generateKeyPair, type JWK } from 'jose';
import { PrismaService } from '../../common/prisma.service';
import { FaydaConfig } from '../../config/fayda.config';
import { VerifaydaService } from './verifayda.service';

function buildPrismaMock() {
  return {
    faydaVerificationSession: {
      create: jest.fn(),
      findUnique: jest.fn(),
      update: jest.fn(),
      updateMany: jest.fn(),
    },
    bookingSeat: {
      updateMany: jest.fn(),
    },
    passenger: { create: jest.fn() },
    loyaltyAccount: { create: jest.fn() },
    walletAccount: { create: jest.fn() },
    userPreferences: { create: jest.fn() },
    verifaydaVerification: { create: jest.fn() },
  };
}

function buildDataSourceMock() {
  return { query: jest.fn().mockResolvedValue([]) };
}

function buildConfig(overrides?: Partial<FaydaConfig>): FaydaConfig {
  return {
    enabled: true,
    clientId: 'edr-test-client',
    authorizationEndpoint: 'https://esignet.test/authorize',
    tokenEndpoint: 'https://esignet.test/token',
    userInfoEndpoint: 'https://esignet.test/userinfo',
    redirectUri: 'http://localhost:4000/fayda/verification/complete',
    webRedirectUri: 'http://localhost:5174/fayda/verification/complete',
    privateJwk: { kty: 'RSA', n: '', e: '', d: '' },
    scope: 'openid profile email',
    acrValues: 'mosip:idp:acr:generated-code',
    claimsLocales: 'en am',
    sessionTtlMinutes: 10,
    ...overrides,
  };
}

function buildConfigService(faydaConfig: FaydaConfig): jest.Mocked<ConfigService> {
  return {
    get: jest.fn((key: string, defaultValue?: unknown) => {
      if (key === 'fayda') return faydaConfig;
      if (key === 'VERIFAYDA_ENABLED') return false;
      return defaultValue;
    }),
  } as unknown as jest.Mocked<ConfigService>;
}

describe('VerifaydaService (OIDC, client-callback)', () => {
  let prisma: ReturnType<typeof buildPrismaMock>;
  let dataSource: ReturnType<typeof buildDataSourceMock>;
  let service: VerifaydaService;
  let realPrivateJwk: JWK;

  beforeAll(async () => {
    const kp = await generateKeyPair('RS256', { extractable: true });
    realPrivateJwk = await exportJWK(kp.privateKey);
    realPrivateJwk.kty = 'RSA';
  });

  beforeEach(() => {
    prisma = buildPrismaMock();
    dataSource = buildDataSourceMock();
    const cfg = buildConfig({ privateJwk: realPrivateJwk as FaydaConfig['privateJwk'] });
    service = new VerifaydaService(
      buildConfigService(cfg),
      prisma as unknown as PrismaService,
      dataSource as any,
    );
    (global as any).fetch = jest.fn();
  });

  afterEach(() => {
    jest.restoreAllMocks();
  });

  describe('startVerification', () => {
    it('persists a session and returns a fully-formed authorize URL', async () => {
      prisma.faydaVerificationSession.create.mockResolvedValue({});

      const url = await service.startVerification({
        purpose: 'VERIFY',
        userId: 'user-1',
      });

      const created = prisma.faydaVerificationSession.create.mock.calls[0][0].data;
      expect(created.purpose).toBe('VERIFY');
      expect(created.platform).toBe('WEB');
      expect(typeof created.state).toBe('string');
      expect(typeof created.codeVerifier).toBe('string');

      const parsed = new URL(url);
      expect(parsed.origin + parsed.pathname).toBe('https://esignet.test/authorize');
      expect(parsed.searchParams.get('client_id')).toBe('edr-test-client');
      expect(parsed.searchParams.get('code_challenge_method')).toBe('S256');
      // Default platform is WEB → webRedirectUri.
      expect(parsed.searchParams.get('redirect_uri')).toBe(
        'http://localhost:5174/fayda/verification/complete',
      );
      expect(parsed.searchParams.get('state')).toBe(created.state);
    });

    it('sends the MOBILE redirect_uri (base redirectUri) for MOBILE sessions', async () => {
      prisma.faydaVerificationSession.create.mockResolvedValue({});

      const url = await service.startVerification({
        purpose: 'LOGIN',
        platform: 'MOBILE',
      });

      const created = prisma.faydaVerificationSession.create.mock.calls[0][0].data;
      expect(created.platform).toBe('MOBILE');
      expect(new URL(url).searchParams.get('redirect_uri')).toBe(
        'http://localhost:4000/fayda/verification/complete',
      );
    });

    it('sends the WEB redirect_uri (webRedirectUri) for WEB sessions', async () => {
      prisma.faydaVerificationSession.create.mockResolvedValue({});

      const url = await service.startVerification({
        purpose: 'VERIFY',
        platform: 'WEB',
      });

      expect(new URL(url).searchParams.get('redirect_uri')).toBe(
        'http://localhost:5174/fayda/verification/complete',
      );
    });

    it('throws ServiceUnavailable when fayda integration is disabled', async () => {
      const disabledService = new VerifaydaService(
        buildConfigService(buildConfig({ enabled: false })),
        prisma as unknown as PrismaService,
        buildDataSourceMock() as any,
      );
      await expect(
        disabledService.startVerification({ purpose: 'VERIFY' }),
      ).rejects.toMatchObject({ status: 503 });
    });
  });

  describe('completeVerification — validation', () => {
    function pendingSession(overrides: Partial<any> = {}) {
      return {
        id: 'session-1',
        state: 'state-abc',
        codeVerifier: 'verifier-xyz',
        purpose: 'VERIFY',
        platform: 'WEB',
        status: 'PENDING',
        errorCode: null,
        errorDescription: null,
        iamUserId: null,
        expiresAt: new Date(Date.now() + 60_000),
        ...overrides,
      };
    }

    it('throws and marks failed when callback carries an error', async () => {
      prisma.faydaVerificationSession.updateMany.mockResolvedValue({ count: 1 });
      await expect(
        service.completeVerification({
          error: 'access_denied',
          error_description: 'user cancelled',
          state: 'state-abc',
        }),
      ).rejects.toMatchObject({ status: 400 });
      expect(prisma.faydaVerificationSession.updateMany).toHaveBeenCalled();
    });

    it('throws FAYDA_MISSING_PARAMETERS when code/state absent', async () => {
      await expect(service.completeVerification({})).rejects.toMatchObject({
        status: 400,
      });
    });

    it('throws FAYDA_INVALID_STATE for unknown state', async () => {
      prisma.faydaVerificationSession.findUnique.mockResolvedValue(null);
      await expect(
        service.completeVerification({ code: 'c', state: 'bogus' }),
      ).rejects.toMatchObject({ status: 400 });
    });

    it('throws FAYDA_INVALID_STATE for a non-pending session', async () => {
      prisma.faydaVerificationSession.findUnique.mockResolvedValue(
        pendingSession({ status: 'COMPLETED' }),
      );
      await expect(
        service.completeVerification({ code: 'c', state: 'state-abc' }),
      ).rejects.toMatchObject({ status: 400 });
    });

    it('throws FAYDA_SESSION_EXPIRED and marks failed for an expired session', async () => {
      prisma.faydaVerificationSession.findUnique.mockResolvedValue(
        pendingSession({ expiresAt: new Date(Date.now() - 1000) }),
      );
      prisma.faydaVerificationSession.updateMany.mockResolvedValue({ count: 1 });
      await expect(
        service.completeVerification({ code: 'c', state: 'state-abc' }),
      ).rejects.toMatchObject({ status: 400 });
      expect(prisma.faydaVerificationSession.updateMany).toHaveBeenCalled();
    });
  });

  describe('completeVerification — VERIFY', () => {
    function pendingSession(overrides: Partial<any> = {}) {
      return {
        id: 'session-1',
        state: 'state-abc',
        codeVerifier: 'verifier-xyz',
        purpose: 'VERIFY',
        platform: 'WEB',
        status: 'PENDING',
        iamUserId: null,
        expiresAt: new Date(Date.now() + 60_000),
        ...overrides,
      };
    }

    function mockFetchSequence(...responses: Array<Partial<Response>>) {
      const queue = responses.map((r) => ({
        ok: true,
        status: 200,
        text: async () => '',
        json: async () => ({}),
        headers: new Headers({ 'content-type': 'application/json' }),
        ...r,
      }));
      (global as any).fetch = jest.fn(() => Promise.resolve(queue.shift()));
    }

    it('returns the verified identity attributes and writes no domain rows', async () => {
      prisma.faydaVerificationSession.findUnique.mockResolvedValue(pendingSession());
      prisma.faydaVerificationSession.update.mockResolvedValue({});

      mockFetchSequence(
        { json: async () => ({ access_token: 'tok', token_type: 'Bearer' }) },
        {
          headers: new Headers({ 'content-type': 'application/json' }),
          text: async () =>
            JSON.stringify({
              sub: 'fayda-sub-1',
              name: 'Test User',
              email: 'test@example.com',
              phone_number: '+251911000000',
              birthdate: '1990-05-01',
              gender: 'Male',
            }),
        },
      );

      const result = await service.completeVerification({
        code: 'authcode',
        state: 'state-abc',
      });

      expect(result).toMatchObject({
        purpose: 'VERIFY',
        verified: true,
        fullName: 'Test User',
        email: 'test@example.com',
        phoneNumber: '+251911000000',
        birthdate: '1990-05-01',
        gender: 'Male',
      });
      expect(result.token).toBeUndefined();
      expect(result.user).toBeUndefined();
      expect(prisma.bookingSeat.updateMany).not.toHaveBeenCalled();
    });

    it('throws 502 when the token endpoint returns 4xx', async () => {
      prisma.faydaVerificationSession.findUnique.mockResolvedValue(pendingSession());
      prisma.faydaVerificationSession.updateMany.mockResolvedValue({ count: 1 });
      mockFetchSequence({
        ok: false,
        status: 400,
        text: async () => '{"error":"invalid_assertion"}',
      });

      await expect(
        service.completeVerification({ code: 'authcode', state: 'state-abc' }),
      ).rejects.toMatchObject({ status: 502 });
    });

    it('throws 502 when userinfo is an unsupported format', async () => {
      prisma.faydaVerificationSession.findUnique.mockResolvedValue(pendingSession());
      prisma.faydaVerificationSession.updateMany.mockResolvedValue({ count: 1 });
      mockFetchSequence(
        { json: async () => ({ access_token: 'tok', token_type: 'Bearer' }) },
        {
          headers: new Headers({ 'content-type': 'text/plain' }),
          text: async () => 'not-a-jwt-not-a-json',
        },
      );

      await expect(
        service.completeVerification({ code: 'authcode', state: 'state-abc' }),
      ).rejects.toMatchObject({ status: 502 });
    });

    it('falls back to localized name (name#en) when name is missing', async () => {
      prisma.faydaVerificationSession.findUnique.mockResolvedValue(pendingSession());
      prisma.faydaVerificationSession.update.mockResolvedValue({});

      mockFetchSequence(
        { json: async () => ({ access_token: 'tok', token_type: 'Bearer' }) },
        {
          headers: new Headers({ 'content-type': 'application/json' }),
          text: async () =>
            JSON.stringify({
              sub: 'fayda-sub-4',
              'name#en': 'English Name',
              'name#am': 'Amharic Name',
            }),
        },
      );

      const result = await service.completeVerification({
        code: 'c',
        state: 'state-abc',
      });
      expect(result.fullName).toBe('English Name');
    });
  });

  describe('completeVerification — LOGIN', () => {
    function loginSession(overrides: Partial<any> = {}) {
      return {
        id: 'login-session',
        state: 'state-login',
        codeVerifier: 'verifier-xyz',
        purpose: 'LOGIN',
        platform: 'WEB',
        status: 'PENDING',
        iamUserId: null,
        expiresAt: new Date(Date.now() + 60_000),
        ...overrides,
      };
    }

    function mockLoginFetch(userInfo: Record<string, unknown>) {
      const queue = [
        {
          ok: true,
          status: 200,
          json: async () => ({ access_token: 'tok', token_type: 'Bearer' }),
          text: async () => '',
          headers: new Headers({ 'content-type': 'application/json' }),
        },
        {
          ok: true,
          status: 200,
          json: async () => ({}),
          text: async () => JSON.stringify(userInfo),
          headers: new Headers({ 'content-type': 'application/json' }),
        },
      ];
      (global as any).fetch = jest.fn(() => Promise.resolve(queue.shift()));
    }

    beforeEach(() => {
      prisma.faydaVerificationSession.findUnique.mockResolvedValue(loginSession());
    });

    it('always rejects with FAYDA_LOGIN_MIGRATED_TO_IAM (401)', async () => {
      mockLoginFetch({ sub: 'login-sub-1', name: 'New Person', email: 'new@example.com' });
      prisma.faydaVerificationSession.updateMany.mockResolvedValue({ count: 1 });

      await expect(
        service.completeVerification({ code: 'c', state: 'state-login' }),
      ).rejects.toMatchObject({
        status: 401,
        response: expect.objectContaining({ code: 'FAYDA_LOGIN_MIGRATED_TO_IAM' }),
      });
    });

    it('does not touch the database for LOGIN purpose', async () => {
      mockLoginFetch({ sub: 'login-sub-2', name: 'Person' });
      prisma.faydaVerificationSession.updateMany.mockResolvedValue({ count: 1 });

      await expect(
        service.completeVerification({ code: 'c', state: 'state-login' }),
      ).rejects.toMatchObject({ status: 401 });
      expect(dataSource.query).not.toHaveBeenCalled();
      expect(prisma.passenger.create).not.toHaveBeenCalled();
    });
  });

  describe('getVerificationStatus', () => {
    it('returns verified=true when IAM user metadata has the flag', async () => {
      dataSource.query.mockResolvedValueOnce([{
        metadata: { faydaVerified: true, faydaVerifiedAt: '2026-01-01T00:00:00.000Z' },
        name: { en: 'Test User', am: 'ቴስት ዩዘር' },
      }]);
      const result = await service.getVerificationStatus('iam-user-1');
      expect(result).toEqual({
        verified: true,
        verifiedAt: new Date('2026-01-01T00:00:00Z'),
        fullName: 'Test User',
      });
    });

    it('returns verified=false when IAM user is missing or unverified', async () => {
      dataSource.query.mockResolvedValueOnce([]);
      const result = await service.getVerificationStatus('iam-user-x');
      expect(result).toEqual({ verified: false });
    });
  });
});
