import { ManagementClient } from 'auth0';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { Auth0ManagementProvider } from '../auth0-management.provider.js';
import type { Environment } from '../../../../shared/types/index.js';

// Mock auth0 library
vi.mock('auth0', () => {
  const users = {
    create: vi.fn(),
    delete: vi.fn(),
    update: vi.fn(),
    get: vi.fn(),
    listUsersByEmail: vi.fn(),
  };
  const tickets = {
    changePassword: vi.fn(),
  };
  const ManagementClient = vi.fn(function() {
    return {
        users,
        tickets,
    }
});
  return { ManagementClient };
});

describe('Auth0ManagementProvider', () => {
  let service: Auth0ManagementProvider;
  let mockClient: any;
  let mockEnv: Environment;

  beforeEach(() => {
    vi.clearAllMocks();
    
    // Create mock environment
    mockEnv = {
      auth0: {
        domain: 'test-domain',
        clientId: 'test-client-id',
        clientSecret: 'test-client-secret',
        audience: 'test-audience',
        managementAudience: 'test-management-audience',
      },
      general: {
        frontendUrl: 'http://localhost:3000',
      },
      // minimal mock
    } as any;

    service = new Auth0ManagementProvider(mockEnv);
    // Get the instance of the mocked client
    mockClient = (ManagementClient as any).mock.results[0].value;
  });

  afterEach(() => {
    vi.resetModules();
  });

  it('should initialize ManagementClient with correct config', () => {
    expect(ManagementClient).toHaveBeenCalledWith({
      domain: 'test-domain',
      clientId: 'test-client-id',
      clientSecret: 'test-client-secret',
      audience: 'test-management-audience',
    });
  });

  it('should return existing user id if user already exists', async () => {
    const email = 'existing@example.com';
    const existingUserId = 'auth0|existing';

    mockClient.users.listUsersByEmail.mockResolvedValue([{ user_id: existingUserId }]);

    const userId = await service.createBlockedUser(email);

    expect(userId).toBe(existingUserId);
    expect(mockClient.users.create).not.toHaveBeenCalled();
  });

  it('should create a blocked user with temporary password', async () => {
    const email = 'test@example.com';
    const mockUserId = 'auth0|123456';

    mockClient.users.listUsersByEmail.mockResolvedValue([]);
    mockClient.users.create.mockResolvedValue({
      user_id: mockUserId,
    });

    const userId = await service.createBlockedUser(email);

    expect(userId).toBe(mockUserId);
    expect(mockClient.users.create).toHaveBeenCalledWith(
      expect.objectContaining({
        connection: 'Username-Password-Authentication',
        email,
        blocked: true,
        email_verified: false,
        app_metadata: {
            registered_by: 'accounter',
        },
      }),
    );
    
    // Verify password complexity requirements
    const callArgs = mockClient.users.create.mock.calls[0][0];
    const password = callArgs.password;
    expect(password.length).toBeGreaterThanOrEqual(8);
    expect(/[a-z]/.test(password)).toBe(true);
    expect(/[A-Z]/.test(password)).toBe(true);
    expect(/[0-9]/.test(password)).toBe(true);
    // eslint-disable-next-line no-useless-escape
    expect(/[!@#$%^&*()_+~`|}{[\]:;?><,./\-=]/.test(password)).toBe(true);
  });

  it('should unblock a user', async () => {
    const userId = 'auth0|123456';
    mockClient.users.update.mockResolvedValue({});

    await service.unblockUser(userId);

    expect(mockClient.users.update).toHaveBeenCalledWith(
      userId,
      { blocked: false }
    );
  });

  it('should delete a user', async () => {
    const userId = 'auth0|123456';
    
    mockClient.users.delete.mockResolvedValue({});

    await service.deleteUser(userId);

    expect(mockClient.users.delete).toHaveBeenCalledWith(userId);
  });

  it('should send password reset email (trigger ticket)', async () => {
    const userId = 'auth0|123456';
    const mockTicket = 'https://auth0.com/ticket';
    
    mockClient.tickets.changePassword.mockResolvedValue({ ticket: mockTicket });

    const ticket = await service.sendPasswordResetEmail(userId);

    // Should return the ticket URL
    expect(ticket).toBe(mockTicket);

    // Should NOT assume verified before reset
    expect(mockClient.users.update).not.toHaveBeenCalledWith(
      { id: userId },
      { email_verified: true }
    );

    // Should trigger password change with mark_email_as_verified: true
    expect(mockClient.tickets.changePassword).toHaveBeenCalledWith(
      expect.objectContaining({
        user_id: userId,
        mark_email_as_verified: true,
        result_url: 'http://localhost:3000/login',
      })
    );
  });

  it('should throw error if creation fails', async () => {
    mockClient.users.listUsersByEmail.mockResolvedValue([]);
    mockClient.users.create.mockRejectedValue(new Error('Auth0 Error'));

    await expect(service.createBlockedUser('fail@test.com')).rejects.toThrow('Failed to create Auth0 user: Auth0 Error');
  });

  it('getUserProfileById returns email + name and caches the result', async () => {
    const userId = 'auth0|profile';
    mockClient.users.get.mockResolvedValue({ email: 'ada@example.com', name: 'Ada Lovelace' });

    const first = await service.getUserProfileById(userId);
    const second = await service.getUserProfileById(userId);

    expect(first).toEqual({ email: 'ada@example.com', name: 'Ada Lovelace' });
    expect(second).toEqual({ email: 'ada@example.com', name: 'Ada Lovelace' });
    // Second call is served from the in-memory cache, not a second Auth0 request.
    expect(mockClient.users.get).toHaveBeenCalledTimes(1);
  });

  it('getUserProfileById returns null on failure and does not cache it', async () => {
    const userId = 'auth0|transient';
    mockClient.users.get.mockRejectedValueOnce(new Error('boom'));

    const failed = await service.getUserProfileById(userId);
    expect(failed).toBeNull();

    // A failed lookup is not cached, so a later call retries Auth0.
    mockClient.users.get.mockResolvedValueOnce({ email: 'x@example.com', name: null });
    const retry = await service.getUserProfileById(userId);

    expect(retry).toEqual({ email: 'x@example.com', name: null });
    expect(mockClient.users.get).toHaveBeenCalledTimes(2);
  });
});
