import { describe, it, expect, vi, beforeEach } from 'vitest';
import { LocalStrategy } from '../strategies/local.strategy';

// Mock passport-local before importing LocalStrategy
vi.mock('passport-local', () => {
  return {
    Strategy: class MockStrategy {
      constructor(_options: any) {}
    },
  };
});

vi.mock('@nestjs/passport', () => {
  return {
    PassportStrategy: (StrategyClass: any) => {
      return class extends StrategyClass {};
    },
  };
});

vi.mock('@nestjs/common', async (importOriginal) => {
  const actual = (await importOriginal()) as any;
  return {
    ...actual,
    Logger: class MockLogger {
      error = vi.fn();
      warn = vi.fn();
      log = vi.fn();
    },
  };
});

vi.mock('@modules/shared', () => {
  class DomainException extends Error {
    constructor(public errorCode: string, message: string) {
      super(message);
      this.name = 'DomainException';
    }
  }
  class UnauthorizedException extends DomainException {
    constructor(message: string) {
      super('UNAUTHORIZED', message);
      this.name = 'UnauthorizedException';
    }
  }
  return {
    DomainException,
    UnauthorizedException,
    normalizeVietnamPhone: (phone: string) => {
      if (!phone) return null;
      if (phone.startsWith('+84') && phone.length === 12) return phone;
      if (phone.startsWith('0') && phone.length === 10) return '+84' + phone.slice(1);
      return null;
    },
  };
});

describe('LocalStrategy', () => {
  let strategy: LocalStrategy;
  let mockUserRepo: {
    findByPhone: ReturnType<typeof vi.fn>;
  };

  beforeEach(() => {
    mockUserRepo = {
      findByPhone: vi.fn(),
    };
    strategy = new LocalStrategy(mockUserRepo as any);
  });

  it('throws when phone number is invalid', async () => {
    await expect(strategy.validate('12345', 'password')).rejects.toThrow(
      'Số điện thoại không hợp lệ',
    );
  });

  it('throws when user is not found', async () => {
    mockUserRepo.findByPhone.mockResolvedValue(null);

    await expect(strategy.validate('0912345678', 'password')).rejects.toThrow(
      'Số điện thoại hoặc mật khẩu không đúng',
    );
  });

  it('throws when user has no password hash', async () => {
    mockUserRepo.findByPhone.mockResolvedValue({
      id: 'user-1',
      passwordHash: null,
      isActive: true,
      deletedAt: null,
      phone: { value: '+84912345678' },
      role: 'BUYER',
    });

    await expect(strategy.validate('0912345678', 'password')).rejects.toThrow(
      'Số điện thoại hoặc mật khẩu không đúng',
    );
  });

  it('throws when user is inactive', async () => {
    mockUserRepo.findByPhone.mockResolvedValue({
      id: 'user-1',
      passwordHash: { compare: vi.fn().mockResolvedValue(true) },
      isActive: false,
      deletedAt: null,
      phone: { value: '+84912345678' },
      role: 'BUYER',
    });

    await expect(strategy.validate('0912345678', 'password')).rejects.toThrow(
      'Tài khoản đã bị vô hiệu hóa',
    );
  });

  it('throws 401 when user is soft-deleted (deletedAt set)', async () => {
    mockUserRepo.findByPhone.mockResolvedValue({
      id: 'user-1',
      passwordHash: { compare: vi.fn().mockResolvedValue(true) },
      isActive: true,
      deletedAt: new Date('2026-01-01T00:00:00.000Z'),
      phone: { value: '+84912345678' },
      role: 'BUYER',
    });

    await expect(strategy.validate('0912345678', 'password')).rejects.toThrow(
      'Tài khoản đã bị xóa',
    );
  });

  it('throws when password is wrong', async () => {
    mockUserRepo.findByPhone.mockResolvedValue({
      id: 'user-1',
      passwordHash: { compare: vi.fn().mockResolvedValue(false) },
      isActive: true,
      deletedAt: null,
      phone: { value: '+84912345678' },
      role: 'BUYER',
    });

    await expect(strategy.validate('0912345678', 'password')).rejects.toThrow(
      'Số điện thoại hoặc mật khẩu không đúng',
    );
  });

  it('returns user info with valid credentials', async () => {
    mockUserRepo.findByPhone.mockResolvedValue({
      id: 'user-1',
      passwordHash: { compare: vi.fn().mockResolvedValue(true) },
      isActive: true,
      deletedAt: null,
      totpEnabled: false,
      phone: { value: '+84912345678' },
      role: 'BUYER',
    });

    const result = await strategy.validate('0912345678', 'P@ssw0rd!');

    expect(result).toEqual({
      id: 'user-1',
      phone: '+84912345678',
      role: 'BUYER',
      isMfaRequired: false,
    });
  });

  it('throws UnauthorizedException when phone is undefined (empty body)', async () => {
    await expect(strategy.validate(undefined as any, undefined as any)).rejects.toThrow(
      'Số điện thoại hoặc mật khẩu không đúng',
    );
  });

  it('throws UnauthorizedException when phone is null', async () => {
    await expect(strategy.validate(null as any, 'password')).rejects.toThrow(
      'Số điện thoại hoặc mật khẩu không đúng',
    );
  });

  it('throws UnauthorizedException when password is empty string', async () => {
    await expect(strategy.validate('0912345678', '')).rejects.toThrow(
      'Số điện thoại hoặc mật khẩu không đúng',
    );
  });

  it('wraps unexpected DB errors as UnauthorizedException', async () => {
    mockUserRepo.findByPhone.mockRejectedValue(new Error('DB connection refused'));

    await expect(strategy.validate('0912345678', 'password')).rejects.toThrow(
      'Số điện thoại hoặc mật khẩu không đúng',
    );
  });

  it('wraps unexpected bcrypt errors as UnauthorizedException', async () => {
    mockUserRepo.findByPhone.mockResolvedValue({
      id: 'user-1',
      passwordHash: { compare: vi.fn().mockRejectedValue(new Error('bcrypt internal error')) },
      isActive: true,
      deletedAt: null,
      phone: { value: '+84912345678' },
      role: 'BUYER',
    });

    await expect(strategy.validate('0912345678', 'password')).rejects.toThrow(
      'Số điện thoại hoặc mật khẩu không đúng',
    );
  });
});
