import { describe, it, expect, vi } from 'vitest';
import { RequestPasswordReset, ConfirmPasswordReset, hashToken } from '../application/password-reset.js';
import { InvalidResetTokenError, isTokenUsable, RESET_TOKEN_TTL_MS } from '../domain/password-reset.js';
import type {
  UserRepository,
  PasswordResetTokenRepository,
  ResetRateLimiter,
  PasswordResetMailer,
  PasswordHasher,
} from '../domain/ports.js';
import type { PasswordResetToken } from '../domain/password-reset.js';

function makeToken(overrides: Partial<PasswordResetToken> = {}): PasswordResetToken {
  return {
    id: 'tok-1',
    userId: 'user-1',
    tokenHash: 'h',
    expiresAt: new Date(Date.now() + RESET_TOKEN_TTL_MS),
    usedAt: null,
    createdAt: new Date(),
    ...overrides,
  };
}

describe('password reset', () => {
  it('hashToken is sha256 hex', () => {
    const a = hashToken('hello');
    const b = hashToken('hello');
    const c = hashToken('hellp');
    expect(a).toBe(b);
    expect(a).not.toBe(c);
    expect(a).toMatch(/^[0-9a-f]{64}$/);
  });

  it('isTokenUsable returns false for used tokens', () => {
    const t = makeToken({ usedAt: new Date() });
    expect(isTokenUsable(t)).toBe(false);
  });

  it('isTokenUsable returns false for expired tokens', () => {
    const t = makeToken({ expiresAt: new Date(Date.now() - 1000) });
    expect(isTokenUsable(t)).toBe(false);
  });

  it('RequestPasswordReset is silent when user does not exist (no enumeration)', async () => {
    const users: UserRepository = {
      create: vi.fn(),
      findByEmail: vi.fn().mockResolvedValue(undefined),
      findById: vi.fn(),
      listUsers: vi.fn(),
      updateUser: vi.fn(),
      deleteUser: vi.fn(),
    };
    const tokens: PasswordResetTokenRepository = {
      invalidateAllForUser: vi.fn(),
      create: vi.fn(),
      findByTokenHash: vi.fn(),
      markUsed: vi.fn(),
    };
    const rateLimiter: ResetRateLimiter = { allow: vi.fn().mockResolvedValue(true) };
    const mailer: PasswordResetMailer = { sendPasswordReset: vi.fn() };
    const audit = vi.fn();

    const useCase = new RequestPasswordReset({
      users,
      tokens,
      rateLimiter,
      mailer,
      buildResetUrl: (t) => `https://app/r/${t}`,
      audit,
    });
    await useCase.execute({ email: 'nobody@example.com' });
    expect(tokens.create).not.toHaveBeenCalled();
    expect(mailer.sendPasswordReset).not.toHaveBeenCalled();
    expect(audit).not.toHaveBeenCalled();
  });

  it('RequestPasswordReset creates a token and emails when user exists', async () => {
    const fakeUser = { id: 'u-1', email: 'a@b.com', role: 'customer' as const, passwordHash: 'x', createdAt: new Date() };
    const users: UserRepository = {
      create: vi.fn(),
      findByEmail: vi.fn().mockResolvedValue(fakeUser),
      findById: vi.fn(),
      listUsers: vi.fn(),
      updateUser: vi.fn(),
      deleteUser: vi.fn(),
    };
    const tokens: PasswordResetTokenRepository = {
      invalidateAllForUser: vi.fn().mockResolvedValue(undefined),
      create: vi.fn().mockResolvedValue(makeToken()),
      findByTokenHash: vi.fn(),
      markUsed: vi.fn(),
    };
    const rateLimiter: ResetRateLimiter = { allow: vi.fn().mockResolvedValue(true) };
    const mailer: PasswordResetMailer = { sendPasswordReset: vi.fn().mockResolvedValue(undefined) };
    const audit = vi.fn();

    const useCase = new RequestPasswordReset({
      users,
      tokens,
      rateLimiter,
      mailer,
      buildResetUrl: (t) => `https://app/r/${t}`,
      audit,
    });
    await useCase.execute({ email: 'a@b.com', ip: '127.0.0.1' });

    expect(tokens.invalidateAllForUser).toHaveBeenCalledWith('u-1');
    expect(tokens.create).toHaveBeenCalledOnce();
    expect(mailer.sendPasswordReset).toHaveBeenCalledOnce();
    expect(audit).toHaveBeenCalledWith(expect.objectContaining({ action: 'password_reset.requested' }));
  });

  it('RequestPasswordReset drops the request silently when rate limited', async () => {
    const users: UserRepository = {
      create: vi.fn(),
      findByEmail: vi.fn(),
      findById: vi.fn(),
      listUsers: vi.fn(),
      updateUser: vi.fn(),
      deleteUser: vi.fn(),
    };
    const tokens: PasswordResetTokenRepository = {
      invalidateAllForUser: vi.fn(),
      create: vi.fn(),
      findByTokenHash: vi.fn(),
      markUsed: vi.fn(),
    };
    const rateLimiter: ResetRateLimiter = { allow: vi.fn().mockResolvedValue(false) };
    const mailer: PasswordResetMailer = { sendPasswordReset: vi.fn() };

    const useCase = new RequestPasswordReset({
      users,
      tokens,
      rateLimiter,
      mailer,
      buildResetUrl: (t) => t,
    });
    await useCase.execute({ email: 'a@b.com' });
    expect(tokens.create).not.toHaveBeenCalled();
    expect(users.findByEmail).not.toHaveBeenCalled();
  });

  it('ConfirmPasswordReset updates password and marks token used on success', async () => {
    const valid = makeToken({ userId: 'u-1', tokenHash: hashToken('tok') });
    const tokens: PasswordResetTokenRepository = {
      invalidateAllForUser: vi.fn(),
      create: vi.fn(),
      findByTokenHash: vi.fn().mockResolvedValue(valid),
      markUsed: vi.fn().mockResolvedValue(undefined),
    };
    const users: UserRepository = {
      create: vi.fn(),
      findByEmail: vi.fn(),
      findById: vi.fn(),
      listUsers: vi.fn(),
      updateUser: vi.fn().mockResolvedValue({ id: 'u-1', email: 'a', role: 'customer', createdAt: new Date() }),
      deleteUser: vi.fn(),
    };
    const hasher: PasswordHasher = { hash: vi.fn().mockResolvedValue('NEWHASH'), verify: vi.fn() };
    const audit = vi.fn();

    const useCase = new ConfirmPasswordReset({ tokens, users, hasher, audit });
    await useCase.execute({ token: 'tok', newPassword: 'newpassword123' });

    expect(hasher.hash).toHaveBeenCalledWith('newpassword123');
    expect(users.updateUser).toHaveBeenCalledWith('u-1', { passwordHash: 'NEWHASH' });
    expect(tokens.markUsed).toHaveBeenCalledWith('tok-1');
    expect(audit).toHaveBeenCalledWith(expect.objectContaining({ action: 'password_reset.confirmed' }));
  });

  it('ConfirmPasswordReset rejects expired/used/missing tokens', async () => {
    const tokens: PasswordResetTokenRepository = {
      invalidateAllForUser: vi.fn(),
      create: vi.fn(),
      findByTokenHash: vi.fn().mockResolvedValue(undefined),
      markUsed: vi.fn(),
    };
    const users: UserRepository = {
      create: vi.fn(),
      findByEmail: vi.fn(),
      findById: vi.fn(),
      listUsers: vi.fn(),
      updateUser: vi.fn(),
      deleteUser: vi.fn(),
    };
    const hasher: PasswordHasher = { hash: vi.fn(), verify: vi.fn() };
    const useCase = new ConfirmPasswordReset({ tokens, users, hasher });
    await expect(
      useCase.execute({ token: 'tok', newPassword: 'newpassword123' }),
    ).rejects.toBeInstanceOf(InvalidResetTokenError);
  });

  it('ConfirmPasswordReset rejects passwords shorter than 8 chars', async () => {
    const tokens: PasswordResetTokenRepository = {
      invalidateAllForUser: vi.fn(),
      create: vi.fn(),
      findByTokenHash: vi.fn(),
      markUsed: vi.fn(),
    };
    const users: UserRepository = {
      create: vi.fn(),
      findByEmail: vi.fn(),
      findById: vi.fn(),
      listUsers: vi.fn(),
      updateUser: vi.fn(),
      deleteUser: vi.fn(),
    };
    const hasher: PasswordHasher = { hash: vi.fn(), verify: vi.fn() };
    const useCase = new ConfirmPasswordReset({ tokens, users, hasher });
    await expect(
      useCase.execute({ token: 'tok', newPassword: 'short' }),
    ).rejects.toBeInstanceOf(InvalidResetTokenError);
  });
});