import { describe, expect, it } from 'vitest';
import { NextRequest } from 'next/server';
import {
  checkRateLimit,
  getClientIp,
  getPublicFormRule,
  normalizePathname,
  resetRateLimitStoreForTests,
} from '@/lib/rate-limit';
import {
  parseMailAddress,
  parseRecipientList,
  parseStrictEmail,
  sanitizeHeaderValue,
} from '@/lib/validation/email';
import { isAllowedHref, isAllowedImageUrl, cmsValueError, safeHref } from '@/lib/safe-url';
import {
  isBotSubmission,
  parseCateringBody,
  parseContactBody,
  parseOrderBody,
  parseReservationBody,
  parseReviewBody,
} from '@/lib/validation/public-forms';
import { FIELD_LIMITS } from '@/lib/validation/limits';
import { canonicalizePhone, phonesMatch } from '@/lib/html-escape';
import { sanitizeErrorMessage } from '@/lib/email/sanitize-error';
import { allowOutboundMail } from '@/lib/email/send-guard';
import { generateOrderRef } from '@/lib/db/services/orderService';
import type { CateringInquirySource } from '@/lib/db/repositories/contactRepository';

function headersFrom(init: Record<string, string>) {
  return new Headers(init);
}

describe('getClientIp', () => {
  it('prefers X-Real-IP over a spoofed X-Forwarded-For prefix', () => {
    const a = getClientIp(
      headersFrom({
        'x-real-ip': '203.0.113.10',
        'x-forwarded-for': '1.1.1.1, 203.0.113.10',
      })
    );
    const b = getClientIp(
      headersFrom({
        'x-real-ip': '203.0.113.10',
        'x-forwarded-for': '8.8.8.8, 203.0.113.10',
      })
    );
    expect(a).toBe('203.0.113.10');
    expect(b).toBe('203.0.113.10');
    expect(a).toBe(b);
  });

  it('uses the rightmost X-Forwarded-For hop when X-Real-IP is absent', () => {
    expect(getClientIp(headersFrom({ 'x-forwarded-for': '1.1.1.1, 203.0.113.50' }))).toBe(
      '203.0.113.50'
    );
  });

  it('does not key the in-memory limiter on a spoofed first hop', () => {
    resetRateLimitStoreForTests();
    const mk = (xff: string) =>
      new NextRequest('http://localhost/api/contact/', {
        method: 'POST',
        headers: {
          'x-real-ip': '198.51.100.7',
          'x-forwarded-for': xff,
        },
      });

    const limit = getPublicFormRule().maxRequests;
    for (let i = 0; i < limit; i++) {
      expect(checkRateLimit(mk(`10.0.0.${i}, 198.51.100.7`))).toBeNull();
    }
    const blocked = checkRateLimit(mk('10.0.0.99, 198.51.100.7'));
    expect(blocked?.status).toBe(429);
  });
});

describe('normalizePathname', () => {
  it('strips trailing slashes so /api/contact/ matches', () => {
    expect(normalizePathname('/api/contact/')).toBe('/api/contact');
    expect(normalizePathname('/api/contact')).toBe('/api/contact');
  });
});

describe('email and headers', () => {
  it('rejects newlines, lists, and spaces in emails', () => {
    expect(parseStrictEmail('a@b.com\nCc:evil@x.com')).toBeNull();
    expect(parseStrictEmail('a@b.com,other@x.com')).toBeNull();
    expect(parseStrictEmail('not-an-email')).toBeNull();
    expect(parseStrictEmail('ok@example.com')).toBe('ok@example.com');
    expect(parseMailAddress('Mezze <ok@example.com>')).toBe('ok@example.com');
    expect(parseRecipientList(['a@example.com, b@example.com'])).toEqual([
      'a@example.com',
      'b@example.com',
    ]);
  });

  it('strips CR/LF/commas from header values', () => {
    expect(sanitizeHeaderValue('Hello\r\nBcc: attacker@x.com')).toBe('HelloBcc: attacker@x.com');
  });

  it('does not allow outbound mail when persist failed', () => {
    expect(allowOutboundMail(false)).toBe(false);
    expect(allowOutboundMail(true)).toBe(true);
  });
});

describe('URL allowlist', () => {
  it('rejects javascript: CTAs', () => {
    expect(isAllowedHref('javascript:alert(1)')).toBe(false);
    expect(cmsValueError('cta2_link', 'javascript:alert(1)')).toBeTruthy();
    expect(safeHref('javascript:alert(1)', '/speisekarte')).toBe('/speisekarte');
  });

  it('rejects private/internal image URLs', () => {
    expect(isAllowedImageUrl('http://127.0.0.1:5432')).toBe(false);
    expect(isAllowedImageUrl('https://127.0.0.1/x')).toBe(false);
    expect(cmsValueError('image_url', 'http://127.0.0.1:5432')).toBeTruthy();
  });

  it('allows relative paths and known image hosts', () => {
    expect(isAllowedHref('/speisekarte')).toBe(true);
    expect(isAllowedImageUrl('/uploads/gallery/11111111-1111-4111-8111-111111111111.jpg')).toBe(
      true
    );
    expect(isAllowedImageUrl('https://images.unsplash.com/photo-123')).toBe(true);
  });
});

describe('public form bounds', () => {
  it('rejects quantity 999999 and huge messages', () => {
    const huge = 'x'.repeat(1_000_001);
    const order = parseOrderBody({
      customer_name: 'Ada',
      customer_phone: '+41791234567',
      pickup_time: '12:00',
      total_amount: 10,
      items: [
        {
          menu_item_id: 1,
          item_name: 'Hummus',
          item_price: 10,
          quantity: 999999,
        },
      ],
      form_started_at: Date.now() - 5000,
    });
    expect(order.errors.length).toBeGreaterThan(0);

    const contact = parseContactBody({
      name: 'Ada',
      email: 'ada@example.com',
      subject: 'feedback',
      message: huge,
      form_started_at: Date.now() - 5000,
    });
    expect(contact.errors.length).toBeGreaterThan(0);
  });

  it('rejects order phone that is not a valid number', () => {
    const parsed = parseOrderBody({
      customer_name: 'Ada',
      customer_phone: 'sdfkjsdf',
      pickup_time: '12:00',
      total_amount: 10,
      items: [
        {
          menu_item_id: 1,
          item_name: 'Hummus',
          item_price: 10,
          quantity: 1,
        },
      ],
      form_started_at: Date.now() - 5000,
    });
    expect(parsed.data).toBeNull();
    expect(parsed.errors.some((err) => err.includes('Telefon'))).toBe(true);
  });

  it('normalizes a Swiss order phone to E.164', () => {
    const parsed = parseOrderBody({
      customer_name: 'Ada',
      customer_phone: '079 123 45 67',
      pickup_time: '12:00',
      total_amount: 10,
      items: [
        {
          menu_item_id: 1,
          item_name: 'Hummus',
          item_price: 10,
          quantity: 1,
        },
      ],
      form_started_at: Date.now() - 5000,
    });
    expect(parsed.errors).toEqual([]);
    expect(parsed.data?.customer_phone).toBe('+41791234567');
  });

  it('rejects contact phone that is not a valid number', () => {
    const parsed = parseContactBody({
      name: 'Ada',
      email: 'ada@example.com',
      phone: 'sdfkjsdf',
      subject: 'feedback',
      message: 'Hello',
      form_started_at: Date.now() - 5000,
    });
    expect(parsed.data).toBeNull();
    expect(parsed.errors.some((err) => err.includes('Telefon'))).toBe(true);
  });

  it('normalizes a Swiss contact phone to E.164 and allows omitting phone', () => {
    const withPhone = parseContactBody({
      name: 'Ada',
      email: 'ada@example.com',
      phone: '079 123 45 67',
      subject: 'feedback',
      message: 'Hello',
      form_started_at: Date.now() - 5000,
    });
    expect(withPhone.errors).toEqual([]);
    expect(withPhone.data?.phone).toBe('+41791234567');

    const withoutPhone = parseContactBody({
      name: 'Ada',
      email: 'ada@example.com',
      subject: 'feedback',
      message: 'Hello',
      form_started_at: Date.now() - 5000,
    });
    expect(withoutPhone.errors).toEqual([]);
    expect(withoutPhone.data?.phone).toBeUndefined();
  });

  it('rejects unknown top-level fields', () => {
    const parsed = parseReservationBody({
      customer_name: 'Ada',
      customer_phone: '+41791234567',
      reservation_date: '2099-01-01',
      reservation_time: '19:00',
      guest_count: 2,
      extra: true,
      form_started_at: Date.now() - 5000,
    });
    expect(parsed.errors.length).toBeGreaterThan(0);
  });

  it('rejects reservation phone that is not a valid number', () => {
    const parsed = parseReservationBody({
      customer_name: 'Ada',
      customer_phone: 'sdfkjsdf',
      reservation_date: '2099-01-01',
      reservation_time: '19:00',
      guest_count: 2,
      form_started_at: Date.now() - 5000,
    });
    expect(parsed.data).toBeNull();
    expect(parsed.errors.some((err) => err.includes('Telefon'))).toBe(true);
  });

  it('normalizes a Swiss reservation phone to E.164', () => {
    const parsed = parseReservationBody({
      customer_name: 'Ada',
      customer_phone: '079 123 45 67',
      reservation_date: '2099-01-01',
      reservation_time: '19:00',
      guest_count: 2,
      form_started_at: Date.now() - 5000,
    });
    expect(parsed.errors).toEqual([]);
    expect(parsed.data?.customer_phone).toBe('+41791234567');
  });

  it('requires address for reservations with 10 or more guests', () => {
    const missing = parseReservationBody({
      customer_name: 'Ada',
      customer_phone: '+41791234567',
      reservation_date: '2099-01-01',
      reservation_time: '19:00',
      guest_count: 10,
      form_started_at: Date.now() - 5000,
    });
    expect(missing.data).toBeNull();
    expect(missing.errors.some((e) => e.includes('Adresse'))).toBe(true);

    const ok = parseReservationBody({
      customer_name: 'Ada',
      customer_phone: '+41791234567',
      reservation_date: '2099-01-01',
      reservation_time: '19:00',
      guest_count: 10,
      customer_address_street: 'Musterstrasse 1',
      customer_address_postal_code: '8000',
      customer_address_city: 'Zürich',
      form_started_at: Date.now() - 5000,
    });
    expect(ok.errors).toEqual([]);
    expect(ok.data?.customer_address_city).toBe('Zürich');
  });

  it('parses guest review body and requires 1–5 stars', () => {
    const ok = parseReviewBody({
      author_name: 'Anna M.',
      quote: 'Sehr gut!',
      rating: 5,
      event_label: 'Abendessen',
      form_started_at: Date.now() - 5000,
    });
    expect(ok.errors).toEqual([]);
    expect(ok.data).toEqual({
      author_name: 'Anna M.',
      quote: 'Sehr gut!',
      rating: 5,
      event_label: 'Abendessen',
    });

    const bad = parseReviewBody({
      author_name: 'Anna M.',
      quote: 'Ok',
      rating: 0,
      form_started_at: Date.now() - 5000,
    });
    expect(bad.data).toBeNull();
    expect(bad.errors.length).toBeGreaterThan(0);
  });

  it('treats filled honeypot as a bot', () => {
    expect(
      isBotSubmission({ website: 'https://spam.test', form_started_at: Date.now() - 5000 })
    ).toBe(true);
    expect(
      isBotSubmission({ hp_field: 'https://spam.test', form_started_at: Date.now() - 5000 })
    ).toBe(true);
    expect(isBotSubmission({ form_started_at: Date.now() - 5000 })).toBe(false);
  });

  it('does not treat a missing timestamp as a bot', () => {
    expect(isBotSubmission({})).toBe(false);
    expect(isBotSubmission({ website: '' })).toBe(false);
  });

  it('treats only instant submits as bots when a timestamp is present', () => {
    expect(isBotSubmission({ form_started_at: Date.now() })).toBe(true);
    expect(isBotSubmission({ form_started_at: Date.now() - 1500 })).toBe(false);
  });

  it('parses catering body and enforces guest / selection bounds', () => {
    const ok = parseCateringBody({
      contact_name: 'Ada',
      contact_email: 'ada@example.com',
      contact_phone: '+41791234567',
      guest_count: 40,
      selected_items: [{ item_id: 1, people: 40 }],
      form_started_at: Date.now() - 5000,
    });
    expect(ok.errors).toEqual([]);
    expect(ok.data?.contact_name).toBe('Ada');
    expect(ok.data?.guest_count).toBe(40);
    expect(ok.data?.selected_items).toEqual([{ item_id: 1, people: 40, quantity: 1 }]);

    const independent = parseCateringBody({
      contact_name: 'Ada',
      contact_email: 'ada@example.com',
      guest_count: 600,
      selected_items: [{ item_id: 1, people: 100 }],
      form_started_at: Date.now() - 5000,
    });
    expect(independent.errors).toEqual([]);
    expect(independent.data?.guest_count).toBe(600);
    expect(independent.data?.selected_items).toEqual([{ item_id: 1, people: 100, quantity: 1 }]);

    const foodLarger = parseCateringBody({
      contact_name: 'Ada',
      contact_email: 'ada@example.com',
      guest_count: 100,
      selected_items: [{ item_id: 1, people: 600 }],
      form_started_at: Date.now() - 5000,
    });
    expect(foodLarger.errors).toEqual([]);
    expect(foodLarger.data?.guest_count).toBe(100);
    expect(foodLarger.data?.selected_items).toEqual([{ item_id: 1, people: 600, quantity: 1 }]);

    const tooFew = parseCateringBody({
      contact_name: 'Ada',
      contact_email: 'ada@example.com',
      guest_count: FIELD_LIMITS.cateringGuestMin - 1,
      form_started_at: Date.now() - 5000,
    });
    expect(tooFew.data).toBeNull();
    expect(tooFew.errors.length).toBeGreaterThan(0);

    const badPeople = parseCateringBody({
      contact_name: 'Ada',
      contact_email: 'ada@example.com',
      selected_items: [{ item_id: 1, people: 5 }],
      form_started_at: Date.now() - 5000,
    });
    expect(badPeople.data).toBeNull();
    expect(badPeople.errors.length).toBeGreaterThan(0);
  });

  it('rejects catering phone that is not a valid number', () => {
    const parsed = parseCateringBody({
      contact_name: 'Ada',
      contact_email: 'ada@example.com',
      contact_phone: 'sdfkjsdf',
      guest_count: 40,
      selected_items: [{ item_id: 1, people: 40 }],
      form_started_at: Date.now() - 5000,
    });
    expect(parsed.data).toBeNull();
    expect(parsed.errors.some((err) => err.includes('Telefon'))).toBe(true);
  });

  it('normalizes a Swiss catering phone to E.164 and allows omitting phone', () => {
    const withPhone = parseCateringBody({
      contact_name: 'Ada',
      contact_email: 'ada@example.com',
      contact_phone: '079 123 45 67',
      guest_count: 40,
      selected_items: [{ item_id: 1, people: 40 }],
      form_started_at: Date.now() - 5000,
    });
    expect(withPhone.errors).toEqual([]);
    expect(withPhone.data?.contact_phone).toBe('+41791234567');

    const withoutPhone = parseCateringBody({
      contact_name: 'Ada',
      contact_email: 'ada@example.com',
      guest_count: 40,
      selected_items: [{ item_id: 1, people: 40 }],
      form_started_at: Date.now() - 5000,
    });
    expect(withoutPhone.errors).toEqual([]);
    expect(withoutPhone.data?.contact_phone).toBeUndefined();
  });

  it('accepts catering inquiry sources used by admin create', () => {
    const sources: CateringInquirySource[] = ['web', 'phone', 'walk_in'];
    expect(sources).toHaveLength(3);
  });
});

describe('phonesMatch', () => {
  it('matches Swiss +41 and local 0 prefix exactly after canonicalize', () => {
    expect(canonicalizePhone('+41 79 123 45 67')).toBe('41791234567');
    expect(canonicalizePhone('0791234567')).toBe('41791234567');
    expect(phonesMatch('+41791234567', '0791234567')).toBe(true);
  });

  it('does not unlock on extra digits or another country prefix', () => {
    expect(phonesMatch('+41791234567', '+4179123456799')).toBe(false);
    expect(phonesMatch('+41791234567', '+33791234567')).toBe(false);
  });
});

describe('order refs and errors', () => {
  it('generates MZ- refs without Math.random', () => {
    const refs = new Set(Array.from({ length: 20 }, () => generateOrderRef()));
    for (const ref of refs) {
      expect(ref).toMatch(/^MZ-[A-Z2-9]{6}$/);
    }
    expect(refs.size).toBeGreaterThan(1);
  });

  it('redacts connection strings in sanitizeErrorMessage', () => {
    const msg = sanitizeErrorMessage(
      new Error('connect postgresql://user:secret@db.internal:5432/mezze failed')
    );
    expect(msg).not.toContain('secret');
    expect(msg).not.toMatch(/postgresql:\/\/[^\s*]{8,}/);
  });
});
