/**
 * Analytics service integration tests - exercise the four computations
 * against a seeded port + assert the cache layer reads/writes correctly.
 */

import { describe, it, expect } from 'vitest';
import { eq, and } from 'drizzle-orm';

import { db } from '@/lib/db';
import { interests, interestBerths } from '@/lib/db/schema/interests';
import { analyticsSnapshots } from '@/lib/db/schema/insights';
import {
  computePipelineFunnel,
  computeOccupancyTimeline,
  computeLeadSourceAttribution,
  getPipelineFunnel,
  refreshSnapshotsForPort,
  ALL_METRICS,
  ALL_RANGES,
  SNAPSHOT_TTL_MS,
} from '@/lib/services/analytics.service';
import { makePort, makeClient, makeBerth, makeYacht } from '../helpers/factories';

describe('analytics service', () => {
  describe('computePipelineFunnel', () => {
    it('aggregates interests by stage with conversion percentages', async () => {
      const port = await makePort();
      const client = await makeClient({ portId: port.id });
      // 3 enquiry, 2 qualified, 1 nurturing
      for (const stage of [
        'enquiry',
        'enquiry',
        'enquiry',
        'qualified',
        'qualified',
        'nurturing',
      ]) {
        await db.insert(interests).values({
          portId: port.id,
          clientId: client.id,
          pipelineStage: stage,
        });
      }

      const result = await computePipelineFunnel(port.id, '30d');

      const enquiry = result.stages.find((s) => s.stage === 'enquiry');
      const qualified = result.stages.find((s) => s.stage === 'qualified');
      const nurturing = result.stages.find((s) => s.stage === 'nurturing');
      expect(enquiry?.count).toBe(3);
      expect(enquiry?.conversionPct).toBe(100);
      expect(qualified?.count).toBe(2);
      expect(qualified?.conversionPct).toBeCloseTo(66.7, 0);
      expect(nurturing?.count).toBe(1);
      expect(nurturing?.conversionPct).toBeCloseTo(33.3, 0);
    });

    it('returns zeros when port has no interests', async () => {
      const port = await makePort();
      const result = await computePipelineFunnel(port.id, '30d');
      expect(result.stages).toHaveLength(7);
      expect(result.stages.every((s) => s.count === 0)).toBe(true);
    });
  });

  describe('computeOccupancyTimeline', () => {
    it('returns 7 points for 7d range with cumulative won-deal occupancy', async () => {
      // Post 2026-05-14 the timeline derives occupancy from won
      // interests (cumulative as of each day) rather than active
      // reservations - see analytics.service.ts comment + PRE-DEPLOY-
      // PLAN § 1.1.3. Fixture: 3 berths, one of which sold 5 days ago.
      const port = await makePort();
      await makeBerth({ portId: port.id });
      await makeBerth({ portId: port.id });
      const client = await makeClient({ portId: port.id });
      await makeYacht({
        portId: port.id,
        ownerType: 'client',
        ownerId: client.id,
      });
      const berth = await makeBerth({ portId: port.id });
      const fiveDaysAgo = new Date(Date.now() - 5 * 86_400_000);
      const [interest] = await db
        .insert(interests)
        .values({
          portId: port.id,
          clientId: client.id,
          pipelineStage: 'contract',
          outcome: 'won',
          outcomeAt: fiveDaysAgo,
        })
        .returning();
      await db.insert(interestBerths).values({
        interestId: interest!.id,
        berthId: berth.id,
        isPrimary: true,
      });

      const result = await computeOccupancyTimeline(port.id, '7d');
      expect(result.points).toHaveLength(7);
      // Last point is today; should reflect 1/3 occupancy.
      const today = result.points[result.points.length - 1]!;
      expect(today.total).toBe(3);
      expect(today.occupied).toBe(1);
      expect(today.occupancyPct).toBeCloseTo(33.3, 0);
    });
  });

  describe('computeLeadSourceAttribution', () => {
    it('counts interests grouped by source descending', async () => {
      const port = await makePort();
      const client = await makeClient({ portId: port.id });
      for (const source of ['website', 'website', 'website', 'manual', 'referral', 'referral']) {
        await db.insert(interests).values({
          portId: port.id,
          clientId: client.id,
          pipelineStage: 'open',
          source,
        });
      }

      const result = await computeLeadSourceAttribution(port.id, '30d');
      expect(result.slices[0]).toEqual({ source: 'website', count: 3 });
      expect(result.slices[1]).toEqual({ source: 'referral', count: 2 });
      expect(result.slices[2]).toEqual({ source: 'manual', count: 1 });
    });

    it('groups null source as "unspecified"', async () => {
      const port = await makePort();
      const client = await makeClient({ portId: port.id });
      await db.insert(interests).values({
        portId: port.id,
        clientId: client.id,
        pipelineStage: 'open',
        source: null,
      });

      const result = await computeLeadSourceAttribution(port.id, '30d');
      expect(result.slices.find((s) => s.source === 'unspecified')?.count).toBe(1);
    });
  });

  describe('cache', () => {
    it('getPipelineFunnel writes a snapshot and returns it on subsequent calls', async () => {
      const port = await makePort();
      const client = await makeClient({ portId: port.id });
      await db.insert(interests).values({
        portId: port.id,
        clientId: client.id,
        pipelineStage: 'open',
      });

      const first = await getPipelineFunnel(port.id, '30d');
      // Snapshot written.
      const row = await db.query.analyticsSnapshots.findFirst({
        where: and(
          eq(analyticsSnapshots.portId, port.id),
          eq(analyticsSnapshots.metricId, 'pipeline_funnel.30d'),
        ),
      });
      expect(row).toBeDefined();
      expect(row?.data).toEqual(first);

      // Mutate the snapshot row directly to confirm cache is being read,
      // not recomputed.
      const sentinel = { stages: [{ stage: 'sentinel', count: 999, conversionPct: 0 }] };
      await db
        .update(analyticsSnapshots)
        .set({ data: sentinel })
        .where(
          and(
            eq(analyticsSnapshots.portId, port.id),
            eq(analyticsSnapshots.metricId, 'pipeline_funnel.30d'),
          ),
        );
      const second = await getPipelineFunnel(port.id, '30d');
      expect(second).toEqual(sentinel);
    });

    it('refreshSnapshotsForPort warms every metric × range combo', async () => {
      const port = await makePort();
      await refreshSnapshotsForPort(port.id);
      const rows = await db
        .select({ metricId: analyticsSnapshots.metricId })
        .from(analyticsSnapshots)
        .where(eq(analyticsSnapshots.portId, port.id));
      const expected = ALL_METRICS.length * ALL_RANGES.length;
      expect(rows).toHaveLength(expected);
    });

    it('snapshot ttl constant is 15 minutes', () => {
      expect(SNAPSHOT_TTL_MS).toBe(15 * 60 * 1000);
    });
  });
});
