import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios';

import type { LaravelMasterUserRow } from './dto/paginated-response.dto';
import type { MasterUser } from './dto/master-user.dto';

import {
  BadGatewayException,
  BadRequestException,
  ConflictException,
  InternalServerErrorException,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import { MasterApiService } from './master-api.service';

function mockAxiosError(
  status: number,
  data: Record<string, unknown>,
  cfg: Partial<AxiosRequestConfig> = {},
): AxiosError {
  const err = new AxiosError(undefined, AxiosError.ERR_BAD_REQUEST);
  const baseConfig = {
    url: '/v1/master/users',
    method: 'get',
    headers: {},
    ...cfg,
  } as AxiosError['config'];
  err.config = baseConfig;
  err.response = {
    status,
    data,
    statusText: 'Error',
    headers: {},
    config: baseConfig!,
  };
  err.isAxiosError = true;
  return err;
}

function makeAxiosClientSpies(createSpyRef: {
  spy: jest.SpyInstance | null;
  post: jest.Mock;
  request: jest.Mock;
}) {
  const post = jest.fn();
  const request = jest.fn();
  createSpyRef.spy?.mockRestore();
  createSpyRef.spy = jest.spyOn(axios, 'create').mockImplementation(
    () =>
      ({
        post,
        request,
        interceptors: {
          request: {
            use: jest.fn(() => 0),
            eject: jest.fn(),
            clear: jest.fn(),
            forEach: jest.fn(),
          },
          response: {
            use: jest.fn(() => 0),
            eject: jest.fn(),
            clear: jest.fn(),
            forEach: jest.fn(),
          },
        },
        defaults: { headers: {} },
      }) as unknown as AxiosInstance,
  );
  createSpyRef.post = post;
  createSpyRef.request = request;
}

function tokenResponseBody() {
  return {
    access_token: 'pfm_testtoken123',
    expires_at: new Date(Date.now() + 7_200_000).toISOString(),
    allowed_methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  };
}

function usersPage(users: LaravelMasterUserRow[]) {
  return {
    data: users,
    meta: {
      current_page: 1,
      per_page: 25,
      total: users.length,
      last_page: 1,
    },
  };
}

const sampleRow: LaravelMasterUserRow = {
  uuid: 'a0000000-0000-4000-8000-000000000001',
  name: 'Ada Lovelace',
  first_name: 'Ada',
  last_name: 'Lovelace',
  email: 'ada@planfi.dev',
  email_verified_at: null,
  phone_country_code: 'BR',
  phone: '11999998888',
  phone_is_whatsapp: false,
  whatsapp: '11988887777',
  whatsapp_country_code: 'BR',
  business_name: 'PlanFi Capital',
  business_subdomain: 'planficapital',
  created_at: '2024-01-01T12:00:00.000000Z',
  updated_at: '2024-01-01T12:00:00.000000Z',
};

function masterUser(email: string, uuid: string): MasterUser {
  const local = email.includes('@') ? (email.split('@')[0] ?? '') : email;

  return {
    uuid,
    name: `${local}`,
    firstName: local || 'nom',
    lastName: 'e',
    email,
    emailVerifiedAt: null,
    phone: null,
    phoneCountryCode: null,
    phoneIsWhatsapp: null,
    whatsapp: null,
    whatsappCountryCode: null,
    businessName: null,
    businessSubdomain: null,
    createdAt: '2024-01-01T12:00:00.000000Z',
    updatedAt: '2024-01-01T12:00:00.000000Z',
  };
}

function configProvider() {
  return {
    provide: ConfigService,
    useValue: {
      get: jest.fn(
        (key: string) =>
          (
            ({
              LARAVEL_URL: 'http://localhost:8000',
              LARAVEL_API_URL: 'http://localhost:8000',
              MASTER_API_SECRET: 'bff-test-secret',
              MASTER_API_ALLOWED_METHODS: [
                'GET',
                'POST',
                'PUT',
                'PATCH',
                'DELETE',
              ],
              MASTER_API_IMPERSONATION_HEADER: 'X-Master-User-UUID',
              MASTER_API_TOKEN_TTL_SECONDS: 3600,
            }) as Record<string, unknown>
          )[key],
      ),
    },
  };
}

describe('MasterApiService', () => {
  const axiosCreateRef = {
    spy: null as jest.SpyInstance | null,
    post: jest.fn(),
    request: jest.fn(),
  };

  beforeEach(() => {
    axiosCreateRef.spy?.mockRestore();
    axiosCreateRef.spy = null;
    jest.clearAllMocks();
    makeAxiosClientSpies(axiosCreateRef);
  });

  afterAll(() => {
    axiosCreateRef.spy?.mockRestore();
  });

  it('NODE_ENV=test emite token mock sem chamada POST /v1/master/token', async () => {
    const prevEnv = process.env.NODE_ENV;
    process.env.NODE_ENV = 'test';

    axiosCreateRef.request.mockResolvedValueOnce({ data: usersPage([]) });

    const module: TestingModule = await Test.createTestingModule({
      providers: [MasterApiService, configProvider()],
    }).compile();

    const service = module.get(MasterApiService);
    await service.listUsers({});
    expect(axiosCreateRef.post).not.toHaveBeenCalled();

    process.env.NODE_ENV = prevEnv;
    await module.close();
  });

  describe('com mock HTTP real (NODE_ENV=development)', () => {
    const savedEnv = process.env.NODE_ENV;

    beforeEach(() => {
      process.env.NODE_ENV = 'development';
    });

    afterEach(() => {
      process.env.NODE_ENV = savedEnv;
    });

    it('issueToken posta para /v1/master/token com secret e methods corretos', async () => {
      axiosCreateRef.post.mockResolvedValueOnce({
        data: tokenResponseBody(),
        status: 200,
      });
      axiosCreateRef.request.mockResolvedValueOnce({ data: usersPage([]) });

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      await service.listUsers({});

      expect(axiosCreateRef.post).toHaveBeenCalledWith('/v1/master/token', {
        secret: 'bff-test-secret',
        methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
      });
      await module.close();
    });

    it('usa cache de token entre duas chamadas listUsers sem novo POST token', async () => {
      axiosCreateRef.post.mockResolvedValue({
        data: tokenResponseBody(),
        status: 200,
      });
      axiosCreateRef.request.mockResolvedValue({ data: usersPage([]) });

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      await service.listUsers({});
      await service.listUsers({});

      expect(axiosCreateRef.post).toHaveBeenCalledTimes(1);
      await module.close();
    });

    it('força novo token quando dentro da margem de 60s da expiração', async () => {
      const soonExpiry = new Date(Date.now() + 30_000).toISOString();
      axiosCreateRef.post
        .mockResolvedValueOnce({
          data: {
            ...tokenResponseBody(),
            expires_at: soonExpiry,
          },
          status: 200,
        })
        .mockResolvedValueOnce({ data: tokenResponseBody(), status: 200 });
      axiosCreateRef.request.mockResolvedValue({ data: usersPage([]) });

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      await service.listUsers({});
      await service.listUsers({});
      expect(axiosCreateRef.post).toHaveBeenCalledTimes(2);
      await module.close();
    });

    it('5 chamadas paralelas a listUsers fazem apenas 1 POST para token', async () => {
      axiosCreateRef.post.mockImplementation(async () =>
        Promise.resolve({
          data: tokenResponseBody(),
          status: 200,
        }),
      );

      axiosCreateRef.request.mockImplementation(
        async () =>
          await new Promise((resolve) =>
            setTimeout(() => resolve({ data: usersPage([]) }), 40),
          ),
      );

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      await Promise.all([
        service.listUsers({}),
        service.listUsers({}),
        service.listUsers({}),
        service.listUsers({}),
        service.listUsers({}),
      ]);

      expect(axiosCreateRef.post).toHaveBeenCalledTimes(1);
      await module.close();
    });

    it('mapeia perPage→per_page e subscriptionStatus→subscription_status', async () => {
      axiosCreateRef.post.mockResolvedValue({
        data: tokenResponseBody(),
        status: 200,
      });

      axiosCreateRef.request.mockResolvedValueOnce({ data: usersPage([]) });

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      await service.listUsers({
        perPage: 42,
        subscriptionStatus: 'active',
      });

      expect(axiosCreateRef.request).toHaveBeenCalledWith(
        expect.objectContaining({
          method: 'GET',
          params: expect.objectContaining({
            per_page: 42,
            subscription_status: 'active',
          }),
        }),
      );
      await module.close();
    });

    it('retorna PaginatedResponse com pagination de meta Laravel', async () => {
      axiosCreateRef.post.mockResolvedValue({
        data: tokenResponseBody(),
        status: 200,
      });
      axiosCreateRef.request.mockResolvedValue({
        data: usersPage([sampleRow]),
      });

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      const res = await service.listUsers({});

      expect(res.data[0]).toMatchObject({
        uuid: sampleRow.uuid,
        firstName: sampleRow.first_name,
        lastName: sampleRow.last_name,
        email: sampleRow.email,
      });
      expect(res.pagination).toEqual({
        currentPage: 1,
        perPage: 25,
        total: 1,
        lastPage: 1,
      });
      await module.close();
    });

    it('401 em listUsers regenera token e tenta mais uma vez com sucesso', async () => {
      axiosCreateRef.post.mockResolvedValue({
        data: tokenResponseBody(),
        status: 200,
      });

      axiosCreateRef.request
        .mockRejectedValueOnce(mockAxiosError(401, {}))
        .mockResolvedValueOnce({ data: usersPage([sampleRow]) });

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      const res = await service.listUsers({});
      expect(res.data).toHaveLength(1);

      await module.close();
    });

    it('401 duas vezes seguidas levanta InternalServerErrorException', async () => {
      axiosCreateRef.post.mockResolvedValue({
        data: tokenResponseBody(),
        status: 200,
      });

      axiosCreateRef.request
        .mockRejectedValueOnce(mockAxiosError(401, {}))
        .mockRejectedValueOnce(mockAxiosError(401, {}));

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      await expect(service.listUsers({})).rejects.toThrow(
        InternalServerErrorException,
      );

      await module.close();
    });

    it('422 levanta BadRequestException', async () => {
      axiosCreateRef.post.mockResolvedValue({
        data: tokenResponseBody(),
        status: 200,
      });

      axiosCreateRef.request.mockRejectedValue(
        mockAxiosError(
          422,
          {
            message: 'Validation failed upstream',
            errors: { per_page: ['Must be ≤ 100'] },
          },
          { url: '/v1/master/users' },
        ),
      );

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      await expect(service.listUsers({})).rejects.toThrow(BadRequestException);

      await module.close();
    });

    it('5xx levanta BadGatewayException', async () => {
      axiosCreateRef.post.mockResolvedValue({
        data: tokenResponseBody(),
        status: 200,
      });

      axiosCreateRef.request.mockRejectedValue(
        mockAxiosError(502, {}, { url: '/v1/users' }),
      );

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      await expect(service.listUsers({})).rejects.toThrow(BadGatewayException);

      await module.close();
    });

    it('403 levanta UnauthorizedException com mensagem upstream', async () => {
      axiosCreateRef.post.mockResolvedValue({
        data: tokenResponseBody(),
        status: 200,
      });

      axiosCreateRef.request.mockRejectedValue(
        mockAxiosError(
          403,
          {
            message: 'Master API is disabled.',
          },
          { url: '/v1/master/users' },
        ),
      );

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      await expect(service.listUsers({})).rejects.toThrow(
        UnauthorizedException,
      );

      await module.close();
    });

    it('401 ao emitir token levanta InternalServerErrorException com mensagem esperada', async () => {
      axiosCreateRef.post.mockRejectedValue(
        mockAxiosError(
          401,
          { message: 'Invalid master API secret.' },
          { url: '/v1/master/token', method: 'post' },
        ),
      );

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      await expect(service.listUsers({})).rejects.toThrow(
        InternalServerErrorException,
      );

      await module.close();
    });

    it('findUserByUuid devolve null quando data está vazia', async () => {
      axiosCreateRef.post.mockResolvedValue({
        data: tokenResponseBody(),
        status: 200,
      });

      axiosCreateRef.request.mockResolvedValueOnce({ data: usersPage([]) });

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      const hit = await service.findUserByUuid(
        '00000000-0000-0000-0000-000000000099',
      );
      expect(hit).toBeNull();
      await module.close();
    });

    it('searchUsersByEmail delega para listUsers com email e per_page=100', async () => {
      axiosCreateRef.post.mockResolvedValue({
        data: tokenResponseBody(),
        status: 200,
      });

      axiosCreateRef.request.mockResolvedValueOnce({
        data: usersPage([
          sampleRow,
          {
            ...sampleRow,
            uuid: '11111111-1111-1111-1111-111111111111',
            email: 'other@domain',
          },
        ]),
      });

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      const hits = await service.searchUsersByEmail('@planfi.dev');
      expect(hits.some((u) => u.uuid === sampleRow.uuid)).toBe(true);
      expect(axiosCreateRef.request).toHaveBeenCalledWith(
        expect.objectContaining({
          params: expect.objectContaining({
            email: '@planfi.dev',
            per_page: 100,
          }),
        }),
      );

      await module.close();
    });
  });

  describe('impersonation', () => {
    const savedEnv = process.env.NODE_ENV;

    beforeEach(() => {
      process.env.NODE_ENV = 'development';
    });

    afterEach(() => {
      process.env.NODE_ENV = savedEnv;
    });

    it('makeImpersonatedRequest envia X-Master-User-UUID com o bearer master', async () => {
      const uuid = 'b2c3d4e5-f6a7-4812-bcde-f12345678901';

      axiosCreateRef.post.mockResolvedValueOnce({
        data: tokenResponseBody(),
        status: 200,
      });
      axiosCreateRef.request.mockResolvedValueOnce({ data: {} });

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();

      const service = module.get(MasterApiService);
      await service.makeImpersonatedRequest(uuid, {
        method: 'GET',
        path: '/v1/portfolios',
      });

      expect(axiosCreateRef.request).toHaveBeenCalledWith(
        expect.objectContaining({
          method: 'GET',
          url: '/v1/portfolios',
          headers: expect.objectContaining({
            Authorization: 'Bearer pfm_testtoken123',
            'X-Master-User-UUID': uuid,
          }),
        }),
      );

      await module.close();
    });

    it('rejeita UUID inválido com BadRequestException', async () => {
      axiosCreateRef.post.mockResolvedValueOnce({
        data: tokenResponseBody(),
        status: 200,
      });

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      await expect(
        service.makeImpersonatedRequest('not-a-real-uuid', {
          method: 'GET',
          path: '/v1/portfolios',
        }),
      ).rejects.toThrow(BadRequestException);

      expect(axiosCreateRef.request).not.toHaveBeenCalled();
      await module.close();
    });
  });

  describe('resolveUuidsByEmails', () => {
    const savedNodeEnv = process.env.NODE_ENV;
    let searchSpy: jest.SpyInstance | undefined;

    afterEach(() => {
      searchSpy?.mockRestore();
      searchSpy = undefined;
      process.env.NODE_ENV = savedNodeEnv;
    });

    it('resolve todos os emails com match exato', async () => {
      process.env.NODE_ENV = 'test';
      makeAxiosClientSpies(axiosCreateRef);

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);
      searchSpy = jest.spyOn(service, 'searchUsersByEmail');
      searchSpy.mockImplementation(async (email: string) => {
        expect(email).toBe('bob@uniq.net');
        return [
          masterUser('bob@uniq.net', 'aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee'),
        ];
      });

      const r = await service.resolveUuidsByEmails([' Bob@uniq.NET ']);

      expect(r.resolved).toEqual([
        {
          email: 'bob@uniq.net',
          uuid: 'aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee',
        },
      ]);
      expect(r.notFound).toHaveLength(0);
      expect(r.ambiguous).toHaveLength(0);
      expect(searchSpy).toHaveBeenCalledTimes(1);

      await module.close();
    });

    it('deduplica emails normalizados e chama master uma vez por email único', async () => {
      process.env.NODE_ENV = 'test';
      makeAxiosClientSpies(axiosCreateRef);

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);
      searchSpy = jest.spyOn(service, 'searchUsersByEmail');

      searchSpy.mockImplementation(async (email: string) => {
        if (email === 'a@dedup.dev')
          return [
            masterUser('a@dedup.dev', '11111111-1111-4111-a111-111111111111'),
          ];
        return [];
      });

      await service.resolveUuidsByEmails(['A@Dedup.dev', '  a@dedup.dev']);

      expect(searchSpy).toHaveBeenCalledTimes(1);
      expect(searchSpy).toHaveBeenCalledWith('a@dedup.dev');

      await module.close();
    });

    it('classifica notFound quando API devolve lista vazia', async () => {
      process.env.NODE_ENV = 'test';
      makeAxiosClientSpies(axiosCreateRef);

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);
      searchSpy = jest.spyOn(service, 'searchUsersByEmail');
      searchSpy.mockResolvedValue([]);

      const r = await service.resolveUuidsByEmails(['gone@mia.net']);

      expect(r.resolved).toHaveLength(0);
      expect(r.notFound).toEqual(['gone@mia.net']);
      expect(searchSpy).toHaveBeenCalledWith('gone@mia.net');

      await module.close();
    });

    it('classifica não encontrados quando só há substring LIKE mas sem igualdade exata', async () => {
      process.env.NODE_ENV = 'test';
      makeAxiosClientSpies(axiosCreateRef);

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);
      searchSpy = jest.spyOn(service, 'searchUsersByEmail');

      searchSpy.mockResolvedValue([
        masterUser('malice@corp.net', '22222222-2222-4222-a222-222222222222'),
        masterUser('alice@corp.net', '33333333-3333-4333-a333-333333333333'),
      ]);

      const r = await service.resolveUuidsByEmails(['alice@corp.net']);

      expect(r.resolved).toEqual([
        {
          email: 'alice@corp.net',
          uuid: '33333333-3333-4333-a333-333333333333',
        },
      ]);
      expect(r.notFound).toHaveLength(0);
      expect(r.ambiguous).toHaveLength(0);

      await module.close();
    });

    it('marca ambiguous quando dois usuários têm mesmo email literal após filtros', async () => {
      process.env.NODE_ENV = 'test';
      makeAxiosClientSpies(axiosCreateRef);

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);
      searchSpy = jest.spyOn(service, 'searchUsersByEmail');

      searchSpy.mockResolvedValue([
        masterUser('twin@test.io', '44444444-4444-4444-a444-444444444441'),
        masterUser('twin@test.io', '44444444-4444-4444-a444-444444444442'),
      ]);

      const r = await service.resolveUuidsByEmails(['twin@test.io']);

      expect(r.resolved).toHaveLength(0);
      expect(r.ambiguous).toEqual(['twin@test.io']);

      await module.close();
    });

    it('retorna arrays vazios para entrada apenas com strings vazias', async () => {
      process.env.NODE_ENV = 'test';
      makeAxiosClientSpies(axiosCreateRef);

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);
      searchSpy = jest.spyOn(service, 'searchUsersByEmail');
      searchSpy.mockResolvedValue([]);

      const r = await service.resolveUuidsByEmails(['', '   ']);

      expect(r.resolved).toHaveLength(0);
      expect(r.notFound).toHaveLength(0);
      expect(searchSpy).not.toHaveBeenCalled();

      await module.close();
    });
  });

  describe('getAdvisorUsageMetrics', () => {
    const savedEnv = process.env.NODE_ENV;

    beforeEach(() => {
      process.env.NODE_ENV = 'development';
    });

    afterEach(() => {
      process.env.NODE_ENV = savedEnv;
    });

    it('impersona assessor e mapeia capabilities-usage', async () => {
      const uuid = 'b2c3d4e5-f6a7-4812-bcde-f12345678901';

      axiosCreateRef.post.mockResolvedValueOnce({
        data: tokenResponseBody(),
        status: 200,
      });
      axiosCreateRef.request
        .mockResolvedValueOnce({ data: usersPage([{ ...sampleRow, uuid }]) })
        .mockResolvedValueOnce({
          data: {
            clients: 12,
            open_finance_connections: 5,
            client_directory_storage_gb: 2.5,
          },
        });

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();

      const service = module.get(MasterApiService);
      const result = await service.getAdvisorUsageMetrics(uuid);

      expect(result.metrics).toEqual({
        clients: 12,
        openFinanceConnections: 5,
        clientDirectoryStorageGb: 2.5,
      });
      expect(axiosCreateRef.request).toHaveBeenCalledWith(
        expect.objectContaining({
          method: 'GET',
          url: '/v1/user/metrics/capabilities-usage',
          headers: expect.objectContaining({
            'X-Master-User-UUID': uuid,
          }),
        }),
      );

      await module.close();
    });

    it('resolve UUID com hífens quando banco PlanFi usa underscores', async () => {
      const dbUuid = '3dedad84_f705_4f43_8c6e_9ca2503dd654';
      const atlasUuid = '3dedad84-f705-4f43-8c6e-9ca2503dd654';

      axiosCreateRef.post.mockResolvedValueOnce({
        data: tokenResponseBody(),
        status: 200,
      });
      axiosCreateRef.request
        .mockResolvedValueOnce({
          data: usersPage([
            {
              ...sampleRow,
              uuid: dbUuid,
            },
          ]),
        })
        .mockResolvedValueOnce({
          data: {
            clients: 3,
            open_finance_connections: 1,
            client_directory_storage_gb: 0.5,
          },
        });

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();

      const service = module.get(MasterApiService);
      await service.getAdvisorUsageMetrics(atlasUuid);

      expect(axiosCreateRef.request).toHaveBeenLastCalledWith(
        expect.objectContaining({
          method: 'GET',
          url: '/v1/user/metrics/capabilities-usage',
          headers: expect.objectContaining({
            'X-Master-User-UUID': dbUuid,
          }),
        }),
      );

      await module.close();
    });

    it('retorna 404 quando usuário de impersonation não existe', async () => {
      const uuid = 'b2c3d4e5-f6a7-4812-bcde-f12345678901';

      axiosCreateRef.post.mockResolvedValueOnce({
        data: tokenResponseBody(),
        status: 200,
      });
      axiosCreateRef.request
        .mockResolvedValueOnce({ data: usersPage([]) })
        .mockResolvedValueOnce({ data: usersPage([]) });

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();

      const service = module.get(MasterApiService);

      await expect(service.getAdvisorUsageMetrics(uuid)).rejects.toThrow(
        NotFoundException,
      );
      expect(axiosCreateRef.request).toHaveBeenCalledTimes(2);
      expect(axiosCreateRef.request).not.toHaveBeenCalledWith(
        expect.objectContaining({
          url: '/v1/user/metrics/capabilities-usage',
        }),
      );

      await module.close();
    });
  });

  describe('lookupUser', () => {
    it('retorna usuário por uuid com campos de contato', async () => {
      axiosCreateRef.post.mockResolvedValue({
        data: tokenResponseBody(),
        status: 200,
      });
      axiosCreateRef.request.mockResolvedValueOnce({
        data: usersPage([sampleRow]),
      });

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      const result = await service.lookupUser({
        uuid: sampleRow.uuid,
      });

      expect(result.user).toEqual({
        uuid: sampleRow.uuid,
        name: sampleRow.name,
        firstName: sampleRow.first_name,
        lastName: sampleRow.last_name,
        email: sampleRow.email,
        emailVerifiedAt: null,
        phone: '11999998888',
        phoneCountryCode: 'BR',
        phoneIsWhatsapp: false,
        whatsapp: '11988887777',
        whatsappCountryCode: 'BR',
        businessName: 'PlanFi Capital',
        businessSubdomain: 'planficapital',
        createdAt: sampleRow.created_at,
        updatedAt: sampleRow.updated_at,
      });

      await module.close();
    });

    it('retorna usuário por email com match exato', async () => {
      axiosCreateRef.post.mockResolvedValue({
        data: tokenResponseBody(),
        status: 200,
      });
      axiosCreateRef.request
        .mockResolvedValueOnce({ data: usersPage([sampleRow]) })
        .mockResolvedValueOnce({ data: usersPage([sampleRow]) });

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      const result = await service.lookupUser({
        email: 'ada@planfi.dev',
      });

      expect(result.user.uuid).toBe(sampleRow.uuid);
      expect(result.user.whatsapp).toBe('11988887777');

      await module.close();
    });

    it('rejeita quando uuid e email são enviados juntos', async () => {
      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      await expect(
        service.lookupUser({
          uuid: sampleRow.uuid,
          email: 'ada@planfi.dev',
        }),
      ).rejects.toThrow(BadRequestException);

      await module.close();
    });

    it('retorna 404 quando uuid não existe', async () => {
      axiosCreateRef.post.mockResolvedValue({
        data: tokenResponseBody(),
        status: 200,
      });
      axiosCreateRef.request
        .mockResolvedValueOnce({ data: usersPage([]) })
        .mockResolvedValueOnce({ data: usersPage([]) });

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      await expect(
        service.lookupUser({
          uuid: '00000000-0000-4000-8000-000000000099',
        }),
      ).rejects.toThrow(NotFoundException);

      await module.close();
    });

    it('retorna 409 quando email é ambíguo', async () => {
      axiosCreateRef.post.mockResolvedValue({
        data: tokenResponseBody(),
        status: 200,
      });
      axiosCreateRef.request.mockResolvedValueOnce({
        data: usersPage([
          sampleRow,
          { ...sampleRow, uuid: '11111111-1111-4111-a111-111111111111' },
        ]),
      });

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      await expect(
        service.lookupUser({
          email: 'ada@planfi.dev',
        }),
      ).rejects.toThrow(ConflictException);

      await module.close();
    });
  });

  describe('batchFetchUsers', () => {
    it('returns found users grouped by how they were looked up', async () => {
      const userA: MasterUser = {
        uuid: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa',
        name: 'Alice',
        firstName: 'Alice',
        lastName: '',
        email: 'alice@example.com',
        emailVerifiedAt: null,
        phone: '+5511999990000',
        phoneCountryCode: 'BR',
        phoneIsWhatsapp: true,
        whatsapp: '+5511999990000',
        whatsappCountryCode: 'BR',
        businessName: 'Alice Corp',
        businessSubdomain: 'alice',
        createdAt: '2024-01-01T00:00:00Z',
        updatedAt: '2024-01-01T00:00:00Z',
      };
      const userB: MasterUser = {
        ...userA,
        uuid: 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbbbb',
        name: 'Bob',
        email: 'bob@example.com',
      };

      const createSpyRef = {
        spy: null as jest.SpyInstance | null,
        post: jest.fn(),
        request: jest.fn(),
      };
      createSpyRef.spy = jest.spyOn(axios, 'create').mockImplementation(
        () =>
          ({
            post: createSpyRef.post,
            request: createSpyRef.request,
            interceptors: {
              request: { use: jest.fn() },
              response: { use: jest.fn() },
            },
          }) as unknown as AxiosInstance,
      );

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      jest
        .spyOn(service as any, 'lookupUser')
        .mockImplementation(
          async ({ uuid, email }: { uuid?: string; email?: string }) => {
            if (uuid === userA.uuid) return { user: userA };
            if (email === 'bob@example.com') return { user: userB };
            throw new NotFoundException('User not found');
          },
        );

      const result = await service.batchFetchUsers(
        [userA.uuid, 'not-found-uuid'],
        ['bob@example.com', 'ghost@example.com'],
      );

      expect(result.foundByUuid).toEqual([
        { requestedUuid: userA.uuid, user: userA },
      ]);
      expect(result.notFoundUuids).toContain('not-found-uuid');
      expect(result.foundByEmail).toEqual([
        { requestedEmail: 'bob@example.com', user: userB },
      ]);
      expect(result.notFoundEmails).toContain('ghost@example.com');

      await module.close();
      createSpyRef.spy?.mockRestore();
    });

    it('returns empty arrays when both inputs are empty', async () => {
      const createSpyRef = {
        spy: null as jest.SpyInstance | null,
        post: jest.fn(),
        request: jest.fn(),
      };
      createSpyRef.spy = jest.spyOn(axios, 'create').mockImplementation(
        () =>
          ({
            post: createSpyRef.post,
            request: createSpyRef.request,
            interceptors: {
              request: { use: jest.fn() },
              response: { use: jest.fn() },
            },
          }) as unknown as AxiosInstance,
      );

      const module: TestingModule = await Test.createTestingModule({
        providers: [MasterApiService, configProvider()],
      }).compile();
      const service = module.get(MasterApiService);

      const result = await service.batchFetchUsers([], []);

      expect(result).toEqual({
        foundByUuid: [],
        foundByEmail: [],
        notFoundUuids: [],
        notFoundEmails: [],
      });

      await module.close();
      createSpyRef.spy?.mockRestore();
    });
  });
});
