import { describe, it, expect, vi, type Mock, beforeEach } from 'vitest';
import { ForgotPasswordUseCase } from './ForgotPasswordUseCase';
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
import { UserId } from '../../domain/value-objects/UserId';
import { User } from '../../domain/entities/User';
import type { IAuthRepository } from '../../domain/repositories/IAuthRepository';
import type { IMagicLinkRepository } from '../../domain/repositories/IMagicLinkRepository';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';

type ForgotPasswordOutput = {
  message: string;
  magicLink?: string | null;
};

describe('ForgotPasswordUseCase', () => {
  let authRepo: {
    findByEmail: Mock;
    save: Mock;
  };
  let magicLinkRepo: {
    checkRateLimit: Mock;
    createPasswordResetRequest: Mock;
  };
  let logger: Logger;
  let output: UseCaseOutputPort<ForgotPasswordOutput> & { present: Mock };
  let useCase: ForgotPasswordUseCase;

  beforeEach(() => {
    authRepo = {
      findByEmail: vi.fn(),
      save: vi.fn(),
    };
    magicLinkRepo = {
      checkRateLimit: vi.fn(),
      createPasswordResetRequest: vi.fn(),
    };
    logger = {
      debug: vi.fn(),
      info: vi.fn(),
      warn: vi.fn(),
      error: vi.fn(),
    } as unknown as Logger;
    output = {
      present: vi.fn(),
    };

    useCase = new ForgotPasswordUseCase(
      authRepo as unknown as IAuthRepository,
      magicLinkRepo as unknown as IMagicLinkRepository,
      logger,
      output,
    );
  });

  it('should create magic link for existing user', async () => {
    const input = { email: 'test@example.com' };
    const user = User.create({
      id: UserId.create(),
      displayName: 'John Smith',
      email: input.email,
    });

    authRepo.findByEmail.mockResolvedValue(user);
    magicLinkRepo.checkRateLimit.mockResolvedValue(Result.ok(undefined));

    const result = await useCase.execute(input);

    expect(authRepo.findByEmail).toHaveBeenCalledWith(EmailAddress.create(input.email));
    expect(magicLinkRepo.checkRateLimit).toHaveBeenCalledWith(input.email);
    expect(magicLinkRepo.createPasswordResetRequest).toHaveBeenCalled();
    expect(output.present).toHaveBeenCalled();
    expect(result.isOk()).toBe(true);
  });

  it('should return success for non-existent email (security)', async () => {
    const input = { email: 'nonexistent@example.com' };

    authRepo.findByEmail.mockResolvedValue(null);
    magicLinkRepo.checkRateLimit.mockResolvedValue(Result.ok(undefined));

    const result = await useCase.execute(input);

    expect(authRepo.findByEmail).toHaveBeenCalledWith(EmailAddress.create(input.email));
    expect(magicLinkRepo.createPasswordResetRequest).not.toHaveBeenCalled();
    expect(output.present).toHaveBeenCalledWith({
      message: 'If an account exists with this email, a password reset link will be sent',
      magicLink: null,
    });
    expect(result.isOk()).toBe(true);
  });

  it('should handle rate limiting', async () => {
    const input = { email: 'test@example.com' };
    const user = User.create({
      id: UserId.create(),
      displayName: 'John Smith',
      email: input.email,
    });

    authRepo.findByEmail.mockResolvedValue(user);
    magicLinkRepo.checkRateLimit.mockResolvedValue(
      Result.err({ code: 'RATE_LIMIT_EXCEEDED', details: { message: 'Rate limited' } })
    );

    const result = await useCase.execute(input);

    expect(result.isErr()).toBe(true);
    const error = result.unwrapErr();
    expect(error.code).toBe('RATE_LIMIT_EXCEEDED');
  });

  it('should validate email format', async () => {
    const input = { email: 'invalid-email' };

    const result = await useCase.execute(input);

    expect(result.isErr()).toBe(true);
    const error = result.unwrapErr();
    expect(error.code).toBe('REPOSITORY_ERROR');
  });

  it('should generate secure tokens', async () => {
    const input = { email: 'test@example.com' };
    const user = User.create({
      id: UserId.create(),
      displayName: 'John Smith',
      email: input.email,
    });

    authRepo.findByEmail.mockResolvedValue(user);
    magicLinkRepo.checkRateLimit.mockResolvedValue(Result.ok(undefined));

    let capturedToken: string | undefined;
    magicLinkRepo.createPasswordResetRequest.mockImplementation((data) => {
      capturedToken = data.token;
      return Promise.resolve();
    });

    await useCase.execute(input);

    expect(capturedToken).toMatch(/^[a-f0-9]{64}$/); // 32 bytes = 64 hex chars
  });

  it('should set correct expiration time (15 minutes)', async () => {
    const input = { email: 'test@example.com' };
    const user = User.create({
      id: UserId.create(),
      displayName: 'John Smith',
      email: input.email,
    });

    authRepo.findByEmail.mockResolvedValue(user);
    magicLinkRepo.checkRateLimit.mockResolvedValue(Result.ok(undefined));

    const beforeCreate = Date.now();
    let capturedExpiresAt: Date | undefined;
    magicLinkRepo.createPasswordResetRequest.mockImplementation((data) => {
      capturedExpiresAt = data.expiresAt;
      return Promise.resolve();
    });

    await useCase.execute(input);

    const afterCreate = Date.now();
    expect(capturedExpiresAt).toBeDefined();
    const timeDiff = capturedExpiresAt!.getTime() - afterCreate;
    
    // Should be approximately 15 minutes (900000ms)
    expect(timeDiff).toBeGreaterThan(890000);
    expect(timeDiff).toBeLessThan(910000);
  });

  it('should return magic link in development mode', async () => {
    const originalEnv = process.env.NODE_ENV;
    process.env.NODE_ENV = 'development';

    const input = { email: 'test@example.com' };
    const user = User.create({
      id: UserId.create(),
      displayName: 'John Smith',
      email: input.email,
    });

    authRepo.findByEmail.mockResolvedValue(user);
    magicLinkRepo.checkRateLimit.mockResolvedValue(Result.ok(undefined));

    await useCase.execute(input);

    expect(output.present).toHaveBeenCalledWith(
      expect.objectContaining({
        magicLink: expect.stringContaining('token='),
      })
    );

    process.env.NODE_ENV = originalEnv ?? 'test';
  });

  it('should not return magic link in production mode', async () => {
    const originalEnv = process.env.NODE_ENV;
    process.env.NODE_ENV = 'production';

    const input = { email: 'test@example.com' };
    const user = User.create({
      id: UserId.create(),
      displayName: 'John Smith',
      email: input.email,
    });

    authRepo.findByEmail.mockResolvedValue(user);
    magicLinkRepo.checkRateLimit.mockResolvedValue(Result.ok(undefined));

    await useCase.execute(input);

    expect(output.present).toHaveBeenCalledWith(
      expect.objectContaining({
        magicLink: null,
      })
    );

    process.env.NODE_ENV = originalEnv ?? 'test';
  });

  it('should handle repository errors', async () => {
    const input = { email: 'test@example.com' };

    authRepo.findByEmail.mockRejectedValue(new Error('Database error'));

    const result = await useCase.execute(input);

    expect(result.isErr()).toBe(true);
    const error = result.unwrapErr();
    expect(error.code).toBe('REPOSITORY_ERROR');
    expect(error.details.message).toContain('Database error');
  });
});