import { beforeEach, describe, expect, it, vi } from 'vitest'

vi.mock('bcryptjs', () => ({
  default: {
    compare: vi.fn(),
    hash: vi.fn(),
  },
}))
vi.mock('../../services/notificationService', () => ({ sendTransactionalEmail: vi.fn() }))
vi.mock('./auth.employee.repo', () => ({
  findEmployeeWithCompanyById: vi.fn(),
  findEmployeeWithCompanyByEmail: vi.fn(),
  findActiveEmployeeByEmail: vi.fn(),
  findEmployeeByResetToken: vi.fn(),
  findEmployeeById: vi.fn(),
  updatePreferredLanguage: vi.fn(),
  resetPassword: vi.fn(),
}))

import bcrypt from 'bcryptjs'
import { AppError } from '../../http/errors'
import { sendTransactionalEmail } from '../../services/notificationService'
import * as repo from './auth.employee.repo'
import * as service from './auth.employee.service'

const employee = {
  id: 'employee_1',
  email: 'agent@example.test',
  firstName: 'Aya',
  lastName: 'Agent',
  role: 'AGENT',
  preferredLanguage: 'fr',
  companyId: 'company_1',
  isActive: true,
  emailVerified: true,
  emailVerificationToken: null,
  passwordHash: 'hash_old',
  company: { name: 'Atlas Cars', slug: 'atlas' },
}

describe('auth.employee.service edge behavior', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    process.env.JWT_SECRET = 'test-secret'
    process.env.JWT_EXPIRY = '8h'
    delete process.env.DASHBOARD_URL
    delete process.env.NEXT_PUBLIC_DASHBOARD_URL
    vi.mocked(bcrypt.compare).mockResolvedValue(true as never)
    vi.mocked(bcrypt.hash).mockResolvedValue('hash_new' as never)
  })

  it('rejects inactive employee sessions before presenting tenant data', async () => {
    vi.mocked(repo.findEmployeeWithCompanyById).mockResolvedValue({ ...employee, isActive: false } as never)

    await expect(service.getMe('employee_1')).rejects.toMatchObject({
      statusCode: 401,
      error: 'unauthenticated',
    })
  })

  it('logs in active employees with a signed token and company context', async () => {
    vi.mocked(repo.findEmployeeWithCompanyByEmail).mockResolvedValue(employee as never)

    const result = await service.login({ email: 'agent@example.test', password: 'correct-password' })

    expect(bcrypt.compare).toHaveBeenCalledWith('correct-password', 'hash_old')
    expect(result).toMatchObject({
      token: expect.any(String),
      employee: {
        id: 'employee_1',
        companyId: 'company_1',
        companyName: 'Atlas Cars',
        companySlug: 'atlas',
      },
    })
  })

  it('rejects login for employees with a pending email verification token', async () => {
    vi.mocked(repo.findEmployeeWithCompanyByEmail).mockResolvedValue({
      ...employee,
      emailVerified: null,
      emailVerificationToken: 'verify-token',
    } as never)

    await expect(service.login({ email: 'agent@example.test', password: 'correct-password' })).rejects.toMatchObject({
      statusCode: 401,
      error: 'email_not_verified',
    })
  })

  it('logs in legacy full-signup owners that were created without a verification token', async () => {
    vi.mocked(repo.findEmployeeWithCompanyByEmail).mockResolvedValue({
      ...employee,
      role: 'OWNER',
      emailVerified: null,
      emailVerificationToken: null,
    } as never)

    const result = await service.login({ email: 'agent@example.test', password: 'correct-password' })

    expect(result).toMatchObject({
      token: expect.any(String),
      employee: {
        id: 'employee_1',
        role: 'OWNER',
      },
    })
  })

  it('distinguishes employees without passwords from wrong credentials', async () => {
    vi.mocked(repo.findEmployeeWithCompanyByEmail).mockResolvedValue({ ...employee, passwordHash: null } as never)

    await expect(service.login({ email: 'agent@example.test', password: 'anything' })).rejects.toMatchObject({
      statusCode: 401,
      error: 'password_not_set',
    })
    expect(bcrypt.compare).not.toHaveBeenCalled()
  })

  it('sends reset links to active employees using the dashboard base path exactly once', async () => {
    process.env.DASHBOARD_URL = 'https://tenant.example.test/app'
    vi.mocked(repo.findActiveEmployeeByEmail).mockResolvedValue(employee as never)
    vi.mocked(sendTransactionalEmail).mockResolvedValue({ success: true } as never)

    await expect(service.forgotPassword('agent@example.test')).resolves.toEqual({
      message: 'If that email is registered, a reset link has been sent.',
    })

    expect(sendTransactionalEmail).toHaveBeenCalledWith(expect.objectContaining({
      to: 'agent@example.test',
      subject: expect.any(String),
      html: expect.stringContaining('https://tenant.example.test/app/dashboard/reset-password?token='),
      text: expect.stringContaining('https://tenant.example.test/app/dashboard/reset-password?token='),
    }))
  })

  it('does not disclose whether forgot-password emails exist', async () => {
    vi.mocked(repo.findActiveEmployeeByEmail).mockResolvedValue(null)

    await expect(service.forgotPassword('missing@example.test')).resolves.toEqual({
      message: 'If that email is registered, a reset link has been sent.',
    })
    expect(sendTransactionalEmail).not.toHaveBeenCalled()
  })

  it('updates employee password from a stored reset token and clears reset state through the repo', async () => {
    vi.mocked(repo.findEmployeeByResetToken).mockResolvedValue(employee as never)

    await expect(service.resetPassword('stored-token', 'new-secure-password')).resolves.toEqual({
      message: 'Password updated successfully. You can now sign in.',
    })

    expect(bcrypt.hash).toHaveBeenCalledWith('new-secure-password', 12)
    expect(repo.resetPassword).toHaveBeenCalledWith('employee_1', 'hash_new')
  })

  it('rejects invalid reset tokens before hashing new passwords', async () => {
    vi.mocked(repo.findEmployeeByResetToken).mockResolvedValue(null)
    vi.mocked(repo.findEmployeeById).mockResolvedValue(null)

    await expect(service.resetPassword('bad-token', 'new-secure-password')).rejects.toMatchObject({
      statusCode: 400,
      error: 'invalid_token',
    })
    expect(bcrypt.hash).not.toHaveBeenCalled()
  })

  it('delegates language preference updates and returns a stable contract', async () => {
    vi.mocked(repo.updatePreferredLanguage).mockResolvedValue({} as never)

    await expect(service.updateLanguage('employee_1', 'ar')).resolves.toEqual({ language: 'ar' })
    expect(repo.updatePreferredLanguage).toHaveBeenCalledWith('employee_1', 'ar')
  })
})
