import crypto from 'crypto';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { Request, Response } from 'express';
import {
  validateTwilioSignature,
  handleTwilioWhatsApp,
  handleApproval,
} from './twilio-whatsapp.js';

// ── Mocks ────────────────────────────────────────────────────────────────────

vi.mock('../redis.js', () => ({
  default: {
    set: vi.fn(),
    get: vi.fn(),
  },
}));

vi.mock('../db.js', () => ({
  getPool: vi.fn(),
}));

vi.mock('../tracking-links.js', () => ({
  createTrackingLink: vi.fn(),
}));

vi.mock('../clients/whatsapp.js', () => ({
  sendMessage: vi.fn().mockResolvedValue({ success: true, message_id: 'SM_reply' }),
}));

import redis from '../redis.js';
import { getPool } from '../db.js';
import { createTrackingLink } from '../tracking-links.js';
import { sendMessage } from '../clients/whatsapp.js';

// ── Test helpers ─────────────────────────────────────────────────────────────

const AUTH_TOKEN = 'test_auth_token_abc123';
const WEBHOOK_URL = 'https://hermes.squaremcp.com/webhook/twilio/whatsapp';
const CUSTOMER_ID = 'cust_lodge_brothers';
const PILOT_PHONE = '+15551234567';
const MSG_SID = 'SM_abc123';

const DEFAULT_PARAMS: Record<string, string> = {
  Body: '1',
  From: `whatsapp:${PILOT_PHONE}`,
  MessageSid: MSG_SID,
  NumMedia: '0',
  To: 'whatsapp:+19547385805',
};

function makeSignature(url: string, params: Record<string, string>): string {
  const sortedParams = Object.keys(params).sort().map(k => k + params[k]).join('');
  return crypto.createHmac('sha1', AUTH_TOKEN).update(url + sortedParams).digest('base64');
}

function makeReq(overrides: {
  body?: Record<string, string>;
  headers?: Record<string, string>;
  hostname?: string;
} = {}): Request {
  const body = overrides.body ?? { ...DEFAULT_PARAMS };
  const sig = makeSignature(WEBHOOK_URL, body);
  const defaultHeaders = { 'x-twilio-signature': sig };
  const headers = overrides.headers !== undefined ? overrides.headers : defaultHeaders;
  return {
    headers,
    body,
    hostname: 'hermes.squaremcp.com',
  } as unknown as Request;
}

function makeRes() {
  const res = {
    status: vi.fn().mockReturnThis(),
    send: vi.fn().mockReturnThis(),
    type: vi.fn().mockReturnThis(),
  };
  return res as unknown as Response & { status: ReturnType<typeof vi.fn>; send: ReturnType<typeof vi.fn>; type: ReturnType<typeof vi.fn> };
}

function mockPool(customerRows: object[], draftRows: object[]) {
  const mockQuery = vi.fn()
    .mockResolvedValueOnce([customerRows])
    .mockResolvedValueOnce([draftRows])
    .mockResolvedValue([[]]);
  (getPool as ReturnType<typeof vi.fn>).mockReturnValue({ query: mockQuery });
  return mockQuery;
}

function makeDraft(overrides: Partial<{
  id: number;
  draft_text_wa: string;
  destination_url: string | null;
  created_at: Date;
}> = {}) {
  return {
    id: 1,
    draft_text_wa: 'Check out our mortgage rates this week!',
    destination_url: 'https://calendly.com/alex/30min',
    created_at: new Date(),
    ...overrides,
  };
}

// ── validateTwilioSignature ───────────────────────────────────────────────────

describe('validateTwilioSignature', () => {
  it('returns true for a valid signature', () => {
    const params = { ...DEFAULT_PARAMS };
    const sig = makeSignature(WEBHOOK_URL, params);
    expect(validateTwilioSignature(AUTH_TOKEN, WEBHOOK_URL, params, sig)).toBe(true);
  });

  it('returns false for an invalid signature', () => {
    const params = { ...DEFAULT_PARAMS };
    const sig = makeSignature(WEBHOOK_URL, params);
    expect(validateTwilioSignature(AUTH_TOKEN, WEBHOOK_URL, params, sig + 'x')).toBe(false);
  });

  it('returns false for a wrong auth token', () => {
    const params = { ...DEFAULT_PARAMS };
    const sig = makeSignature(WEBHOOK_URL, params);
    expect(validateTwilioSignature('wrong_token', WEBHOOK_URL, params, sig)).toBe(false);
  });

  it('returns false (not throws) when signature is very short — length guard', () => {
    const params = { ...DEFAULT_PARAMS };
    expect(validateTwilioSignature(AUTH_TOKEN, WEBHOOK_URL, params, 'short')).toBe(false);
  });

  it('returns false for empty signature string', () => {
    expect(validateTwilioSignature(AUTH_TOKEN, WEBHOOK_URL, DEFAULT_PARAMS, '')).toBe(false);
  });
});

// ── handleTwilioWhatsApp (route handler) ────────────────────────────────────

describe('handleTwilioWhatsApp', () => {
  beforeEach(() => {
    vi.unstubAllEnvs();
    vi.clearAllMocks();
    process.env.TWILIO_AUTH_TOKEN = AUTH_TOKEN;
    process.env.PUBLIC_WEBHOOK_BASE_URL = 'https://hermes.squaremcp.com';
    (redis.get as ReturnType<typeof vi.fn>).mockResolvedValue(CUSTOMER_ID);
    (redis.set as ReturnType<typeof vi.fn>).mockResolvedValue('OK');
    (getPool as ReturnType<typeof vi.fn>).mockReturnValue({ query: vi.fn().mockResolvedValue([[]]) });
  });

  it('returns 503 when TWILIO_AUTH_TOKEN is not set', async () => {
    delete process.env.TWILIO_AUTH_TOKEN;
    const req = makeReq();
    const res = makeRes();
    await handleTwilioWhatsApp(req, res as unknown as Response);
    expect(res.status).toHaveBeenCalledWith(503);
  });

  it('returns 403 when X-Twilio-Signature header is missing', async () => {
    const req = makeReq({ headers: {} });
    const res = makeRes();
    await handleTwilioWhatsApp(req, res as unknown as Response);
    expect(res.status).toHaveBeenCalledWith(403);
    expect(res.send).toHaveBeenCalledWith('Missing signature');
  });

  it('returns 403 for an invalid signature', async () => {
    const req = makeReq({ headers: { 'x-twilio-signature': 'bad_signature_value' } });
    const res = makeRes();
    await handleTwilioWhatsApp(req, res as unknown as Response);
    expect(res.status).toHaveBeenCalledWith(403);
    expect(res.send).toHaveBeenCalledWith('Invalid signature');
  });

  it('returns 403 (not 500) for a malformed short signature', async () => {
    const req = makeReq({ headers: { 'x-twilio-signature': 'x' } });
    const res = makeRes();
    await handleTwilioWhatsApp(req, res as unknown as Response);
    expect(res.status).toHaveBeenCalledWith(403);
    expect(res.send).toHaveBeenCalledWith('Invalid signature');
  });

  it('returns 200 with TwiML for a valid request', async () => {
    const req = makeReq();
    const res = makeRes();
    await handleTwilioWhatsApp(req, res as unknown as Response);
    expect(res.status).toHaveBeenCalledWith(200);
    expect(res.send).toHaveBeenCalledWith(expect.stringContaining('<Response>'));
    expect(res.type).toHaveBeenCalledWith('text/xml');
  });

  it('uses PUBLIC_WEBHOOK_BASE_URL for signature URL construction', async () => {
    // The default req uses WEBHOOK_URL derived from PUBLIC_WEBHOOK_BASE_URL
    // If PUBLIC_WEBHOOK_BASE_URL is correct, the valid signature validates
    const req = makeReq();
    const res = makeRes();
    await handleTwilioWhatsApp(req, res as unknown as Response);
    expect(res.status).toHaveBeenCalledWith(200);
  });
});

// ── handleApproval ──────────────────────────────────────────────────────────

describe('handleApproval', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    process.env.PUBLIC_WEBHOOK_BASE_URL = 'https://hermes.squaremcp.com';
    (redis.set as ReturnType<typeof vi.fn>).mockResolvedValue('OK');
  });

  it('skips processing for a duplicate MessageSid', async () => {
    (redis.set as ReturnType<typeof vi.fn>).mockResolvedValue(null); // NX returns null when key exists
    (getPool as ReturnType<typeof vi.fn>).mockReturnValue({ query: vi.fn() });
    await handleApproval(CUSTOMER_ID, PILOT_PHONE, MSG_SID, '1', 0);
    expect(getPool().query).not.toHaveBeenCalled();
  });

  it('ignores messages from senders not matching pilot_owner_phone', async () => {
    mockPool([{ pilot_owner_phone: '+19999999999' }], []);
    await handleApproval(CUSTOMER_ID, PILOT_PHONE, MSG_SID, '1', 0);
    expect(sendMessage).not.toHaveBeenCalled();
  });

  it('replies asking to text when NumMedia > 0', async () => {
    mockPool([{ pilot_owner_phone: PILOT_PHONE }], []);
    await handleApproval(CUSTOMER_ID, PILOT_PHONE, MSG_SID, '', 1);
    expect(sendMessage).toHaveBeenCalledWith(expect.objectContaining({
      to: PILOT_PHONE,
      message: expect.stringContaining('1, 2, or 3'),
    }));
  });

  it('approves the correct draft, creates tracking link, and sends confirmation', async () => {
    const draft = makeDraft({ id: 42, destination_url: 'https://calendly.com/alex' });
    (createTrackingLink as ReturnType<typeof vi.fn>).mockResolvedValue('tok_abc123456789012345678901');
    const mockQuery = vi.fn()
      .mockResolvedValueOnce([[{ pilot_owner_phone: PILOT_PHONE }]])
      .mockResolvedValueOnce([[draft]])
      .mockResolvedValue([[]]); // UPDATE
    (getPool as ReturnType<typeof vi.fn>).mockReturnValue({ query: mockQuery });

    await handleApproval(CUSTOMER_ID, PILOT_PHONE, MSG_SID, '1', 0);

    expect(createTrackingLink).toHaveBeenCalledWith(expect.objectContaining({
      customerId: CUSTOMER_ID,
      draftId: 42,
      destinationUrl: 'https://calendly.com/alex',
      expiresAt: null,
    }));
    expect(sendMessage).toHaveBeenCalledWith(expect.objectContaining({
      to: PILOT_PHONE,
      message: expect.stringContaining('Track clicks'),
    }));
    // Verify draft was marked approved
    expect(mockQuery).toHaveBeenCalledWith(
      expect.stringContaining("status = 'approved'"),
      [42]
    );
  });

  it('replies with expiry message for drafts older than 7 days', async () => {
    const oldDate = new Date(Date.now() - 8 * 24 * 60 * 60 * 1000);
    const draft = makeDraft({ created_at: oldDate });
    mockPool([{ pilot_owner_phone: PILOT_PHONE }], [draft]);

    await handleApproval(CUSTOMER_ID, PILOT_PHONE, MSG_SID, '1', 0);

    expect(sendMessage).toHaveBeenCalledWith(expect.objectContaining({
      message: expect.stringContaining('expired'),
    }));
    expect(createTrackingLink).not.toHaveBeenCalled();
  });

  it('replies with help listing pending drafts for unrecognized text', async () => {
    const draft = makeDraft({ draft_text_wa: 'Big sale on mortgage rates this week!' });
    mockPool([{ pilot_owner_phone: PILOT_PHONE }], [draft]);

    await handleApproval(CUSTOMER_ID, PILOT_PHONE, MSG_SID, 'hello', 0);

    expect(sendMessage).toHaveBeenCalledWith(expect.objectContaining({
      message: expect.stringContaining('1, 2, or 3'),
    }));
    expect(createTrackingLink).not.toHaveBeenCalled();
  });

  it('sends a no-drafts message when there are no pending drafts', async () => {
    mockPool([{ pilot_owner_phone: PILOT_PHONE }], []);

    await handleApproval(CUSTOMER_ID, PILOT_PHONE, MSG_SID, '1', 0);

    expect(sendMessage).toHaveBeenCalledWith(expect.objectContaining({
      message: expect.stringContaining('No pending drafts'),
    }));
  });
});
