/**
 * Tests for interest scoring pure helper functions.
 * The exported `calculateInterestScore` hits the database, so we test the
 * scoring logic via the module-private helpers by re-implementing them inline
 * here (they are not exported from the module). Alternatively we test the
 * boundary conditions via vi.mock of the db/redis dependencies and exercising
 * the main function.
 */
import { describe, it, expect, vi, beforeEach } from 'vitest';

// ─── Mock heavy dependencies before importing the service ────────────────────

vi.mock('@/lib/db', () => ({
  db: {
    query: {
      interests: { findFirst: vi.fn() },
    },
    select: vi.fn(),
  },
}));

vi.mock('@/lib/redis', () => ({
  redis: {
    get: vi.fn().mockResolvedValue(null),
    setex: vi.fn().mockResolvedValue('OK'),
  },
}));

vi.mock('@/lib/logger', () => ({
  logger: { warn: vi.fn(), error: vi.fn() },
}));

// Mock drizzle helpers used in the service (count, eq, gte, etc.)
vi.mock('drizzle-orm', async (importOriginal) => {
  const actual = await importOriginal<typeof import('drizzle-orm')>();
  return { ...actual };
});

vi.mock('@/lib/db/schema/interests', () => ({
  interests: {},
  interestBerths: {},
  interestNotes: {},
}));

vi.mock('@/lib/db/schema/operations', () => ({
  reminders: {},
}));

vi.mock('@/lib/db/schema/email', () => ({
  emailThreads: {},
}));

// next/server is not available in the vitest node environment
vi.mock('next/server', () => ({
  NextResponse: { json: vi.fn() },
}));

import { calculateInterestScore } from '@/lib/services/interest-scoring.service';
import { db } from '@/lib/db';
import { redis } from '@/lib/redis';

// ─── Helpers ─────────────────────────────────────────────────────────────────

/** Create a fake db.select chain that returns a fixed count result. */
function makeSelectChain(countValue: number) {
  const chain = {
    from: vi.fn().mockReturnThis(),
    where: vi.fn().mockResolvedValue([{ value: countValue }]),
  };
  return chain;
}

/**
 * Create a sequence of fake db.select chains so the four parallel queries
 * inside calculateInterestScore (notes / reminders / email / interest_berths)
 * can each be given a distinct count. Order matches the call order in the
 * service.
 */
function makeSelectChainSequence(counts: {
  notes: number;
  reminders: number;
  email: number;
  berthLinks: number;
}) {
  const sequence = [counts.notes, counts.reminders, counts.email, counts.berthLinks];
  let i = 0;
  return () => {
    const value = sequence[i++] ?? 0;
    return makeSelectChain(value);
  };
}

function daysAgo(days: number): Date {
  return new Date(Date.now() - days * 24 * 60 * 60 * 1000);
}

// ─── Tests ───────────────────────────────────────────────────────────────────

describe('calculateInterestScore', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    (redis.get as ReturnType<typeof vi.fn>).mockResolvedValue(null);
    (redis.setex as ReturnType<typeof vi.fn>).mockResolvedValue('OK');
  });

  it('score is always in the range 0-100', async () => {
    // Worst-case scenario: interest created 365 days ago, no docs, no engagement
    (db.query.interests.findFirst as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: 'i1',
      portId: 'p1',
      clientId: 'c1',
      createdAt: daysAgo(365),
      pipelineStage: 'open',
      eoiStatus: null,
      contractStatus: null,
      depositStatus: null,
      dateEoiSigned: null,
      dateContractSigned: null,
      dateDepositReceived: null,
      berthId: null,
    });

    const selectChain = makeSelectChain(0);
    (db.select as ReturnType<typeof vi.fn>).mockReturnValue(selectChain);

    const result = await calculateInterestScore('i1', 'p1');
    expect(result.totalScore).toBeGreaterThanOrEqual(0);
    expect(result.totalScore).toBeLessThanOrEqual(100);
  });

  it('new interest (0 days, no docs, no engagement) → low total score', async () => {
    (db.query.interests.findFirst as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: 'i1',
      portId: 'p1',
      clientId: 'c1',
      createdAt: daysAgo(0),
      pipelineStage: 'open',
      eoiStatus: null,
      contractStatus: null,
      depositStatus: null,
      dateEoiSigned: null,
      dateContractSigned: null,
      dateDepositReceived: null,
      berthId: null,
    });

    const selectChain = makeSelectChain(0);
    (db.select as ReturnType<typeof vi.fn>).mockReturnValue(selectChain);

    const result = await calculateInterestScore('i1', 'p1');
    // pipelineAge=100, stageSpeed=0 (still open), docs=0, engagement=0, berth=0
    // raw = 100/425*100 ≈ 24
    expect(result.totalScore).toBeLessThan(30);
    expect(result.breakdown.stageSpeed).toBe(0);
    expect(result.breakdown.documentCompleteness).toBe(0);
    expect(result.breakdown.engagement).toBe(0);
    expect(result.breakdown.berthLinked).toBe(0);
  });

  it('interest with all docs signed and berth linked → high total score', async () => {
    (db.query.interests.findFirst as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: 'i2',
      portId: 'p1',
      clientId: 'c1',
      createdAt: daysAgo(10),
      pipelineStage: 'contract_signed',
      eoiStatus: 'signed',
      contractStatus: 'signed',
      depositStatus: 'received',
      dateEoiSigned: daysAgo(5),
      dateContractSigned: daysAgo(3),
      dateDepositReceived: daysAgo(1),
      berthId: 'berth-1',
    });

    // High engagement: 5 notes, 3 emails, 2 reminders. The 4th query is the
    // interest_berths count (plan §3.4 - berthLinked is derived from any
    // junction row).
    const selectChain = {
      from: vi.fn().mockReturnThis(),
      where: vi
        .fn()
        .mockResolvedValueOnce([{ value: 5 }]) // notes
        .mockResolvedValueOnce([{ value: 2 }]) // reminders
        .mockResolvedValueOnce([{ value: 3 }]) // emails
        .mockResolvedValueOnce([{ value: 1 }]), // interest_berths (≥1 = linked)
    };
    (db.select as ReturnType<typeof vi.fn>).mockReturnValue(selectChain);

    const result = await calculateInterestScore('i2', 'p1');
    expect(result.totalScore).toBeGreaterThan(60);
    expect(result.breakdown.documentCompleteness).toBe(100);
    expect(result.breakdown.berthLinked).toBe(25);
  });

  it('pipeline age: interest created 0-30 days ago → pipelineAge = 100', async () => {
    (db.query.interests.findFirst as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: 'i3',
      portId: 'p1',
      clientId: 'c1',
      createdAt: daysAgo(15),
      pipelineStage: 'open',
      eoiStatus: null,
      contractStatus: null,
      depositStatus: null,
      dateEoiSigned: null,
      dateContractSigned: null,
      dateDepositReceived: null,
      berthId: null,
    });

    const selectChain = makeSelectChain(0);
    (db.select as ReturnType<typeof vi.fn>).mockReturnValue(selectChain);

    const result = await calculateInterestScore('i3', 'p1');
    expect(result.breakdown.pipelineAge).toBe(100);
  });

  it('pipeline age: interest created 180+ days ago → pipelineAge = 20', async () => {
    (db.query.interests.findFirst as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: 'i4',
      portId: 'p1',
      clientId: 'c1',
      createdAt: daysAgo(200),
      pipelineStage: 'open',
      eoiStatus: null,
      contractStatus: null,
      depositStatus: null,
      dateEoiSigned: null,
      dateContractSigned: null,
      dateDepositReceived: null,
      berthId: null,
    });

    const selectChain = makeSelectChain(0);
    (db.select as ReturnType<typeof vi.fn>).mockReturnValue(selectChain);

    const result = await calculateInterestScore('i4', 'p1');
    expect(result.breakdown.pipelineAge).toBe(20);
  });

  it('document completeness: only EOI signed → score = 30', async () => {
    (db.query.interests.findFirst as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: 'i5',
      portId: 'p1',
      clientId: 'c1',
      createdAt: daysAgo(10),
      pipelineStage: 'open',
      eoiStatus: 'signed',
      contractStatus: null,
      depositStatus: null,
      dateEoiSigned: daysAgo(5),
      dateContractSigned: null,
      dateDepositReceived: null,
      berthId: null,
    });

    const selectChain = makeSelectChain(0);
    (db.select as ReturnType<typeof vi.fn>).mockReturnValue(selectChain);

    const result = await calculateInterestScore('i5', 'p1');
    expect(result.breakdown.documentCompleteness).toBe(30);
  });

  it('berthLinked is 25 when interest_berths has any row, 0 when none', async () => {
    // After the M:M refactor (plan §3.4) "berth linked" is derived from
    // the interest_berths junction count rather than the legacy
    // interests.berth_id column. The score awards 25 for any junction
    // row, 0 for none.
    const base = {
      portId: 'p1',
      clientId: 'c1',
      createdAt: daysAgo(10),
      pipelineStage: 'open',
      eoiStatus: null,
      contractStatus: null,
      depositStatus: null,
      dateEoiSigned: null,
      dateContractSigned: null,
      dateDepositReceived: null,
    };

    // ── With a junction row ─────────────────────────────────────────────────
    (db.select as ReturnType<typeof vi.fn>).mockImplementation(
      makeSelectChainSequence({ notes: 0, reminders: 0, email: 0, berthLinks: 1 }),
    );
    (db.query.interests.findFirst as ReturnType<typeof vi.fn>).mockResolvedValue({
      ...base,
      id: 'i6',
    });
    const withBerth = await calculateInterestScore('i6', 'p1');
    expect(withBerth.breakdown.berthLinked).toBe(25);

    // ── Without a junction row ──────────────────────────────────────────────
    (redis.get as ReturnType<typeof vi.fn>).mockResolvedValue(null);
    (db.select as ReturnType<typeof vi.fn>).mockImplementation(
      makeSelectChainSequence({ notes: 0, reminders: 0, email: 0, berthLinks: 0 }),
    );
    (db.query.interests.findFirst as ReturnType<typeof vi.fn>).mockResolvedValue({
      ...base,
      id: 'i7',
    });
    const withoutBerth = await calculateInterestScore('i7', 'p1');
    expect(withoutBerth.breakdown.berthLinked).toBe(0);
  });

  it('throws when interest not found', async () => {
    (db.query.interests.findFirst as ReturnType<typeof vi.fn>).mockResolvedValue(null);
    await expect(calculateInterestScore('missing', 'p1')).rejects.toThrow(
      /couldn't find that interest/,
    );
  });

  it('returns cached result when redis has a hit (after port-scope DB check)', async () => {
    // Security fix: the DB lookup runs FIRST to confirm the interest is
    // in the caller's port. Only then is the (port-scoped) cache key read.
    // A test that asserts the DB is bypassed would be asserting the
    // pre-fix bug; this test asserts the new ordering.
    const cachedScore = {
      totalScore: 42,
      breakdown: {
        pipelineAge: 80,
        stageSpeed: 0,
        documentCompleteness: 0,
        engagement: 0,
        berthLinked: 0,
      },
      calculatedAt: new Date().toISOString(),
    };
    (db.query.interests.findFirst as ReturnType<typeof vi.fn>).mockResolvedValue({
      id: 'cached-id',
      portId: 'p1',
      clientId: 'c1',
      createdAt: daysAgo(10),
      pipelineStage: 'open',
      eoiStatus: null,
      contractStatus: null,
      depositStatus: null,
      dateEoiSigned: null,
      dateContractSigned: null,
      dateDepositReceived: null,
      berthId: null,
    });
    (redis.get as ReturnType<typeof vi.fn>).mockResolvedValue(JSON.stringify(cachedScore));

    const result = await calculateInterestScore('cached-id', 'p1');
    expect(result.totalScore).toBe(42);
    // Port-scope check: the DB IS hit, but no other queries (notes/threads)
    // are needed since the cache served the score body.
    expect(db.query.interests.findFirst).toHaveBeenCalled();
  });
});
