import { describe, it, expect, vi, beforeEach } from 'vitest';
import { parseSseLine } from '../fleet-connector.js';
import type { LlamaSweepSSEEvent, MetricsEntry, ModelStatusEntry } from '../fleet-connector.js';
import { createFleetState, ensureHostState, incrementSeq } from '../fleet-state.js';
import { createDeltaEmitter } from '../delta-emitter.js';
import { handleLlamaSweepEvent } from '../sse-pipeline.js';
import type { DeltaEmitter } from '../delta-emitter.js';
import type { Sql } from '../../db.js';
import type { Config } from '../../config.js';

// ─── SSE parser tests (REAL wire shapes from apigroup.go) ────────────────────
// Real format: event:message / data:{"type":"<TYPE>","data":"<ESCAPED JSON>"}

describe('parseSseLine (real wire shapes)', () => {
  it('parses double-encoded modelStatus (real full-fleet array payload)', () => {
    const inner = JSON.stringify([
      { id: 'llama3', name: '', description: '', state: 'ready', unlisted: false, peerID: '' },
    ]);
    const outer = JSON.stringify({ type: 'modelStatus', data: inner });
    const result = parseSseLine(`data: ${outer}`);
    expect(result).not.toBeNull();
    expect(result!.type).toBe('modelStatus');
    expect(result!.data).toEqual([
      { id: 'llama3', name: '', description: '', state: 'ready', unlisted: false, peerID: '' },
    ]);
  });

  it('ignores event: lines (always event:message)', () => {
    expect(parseSseLine('event:message')).toBeNull();
  });

  it('returns null for data: with missing inner data field', () => {
    expect(parseSseLine('data:{"type":"modelStatus"}')).toBeNull();
  });

  it('returns null for empty line', () => {
    expect(parseSseLine('')).toBeNull();
    expect(parseSseLine('   ')).toBeNull();
  });

  it('returns null for malformed JSON', () => {
    expect(parseSseLine('data: not-json')).toBeNull();
  });
});

// ─── Pipeline integration test (real functions) ──────────────────────────────


function apiModel(id: string, state: string): ModelStatusEntry {
  return { id, name: '', description: '', state, unlisted: false, peerID: '' };
}

describe('SSE pipeline: parse -> handleLlamaSweepEvent -> emit deltas', () => {
  let mockSql: Sql;
  let mockConfig: Config;
  let executedQueries: string[];

  beforeEach(() => {
    executedQueries = [];
    mockSql = Object.assign(
      (strings: TemplateStringsArray, ...values: unknown[]) => {
        const query = strings.reduce((acc: string, s: string, i: number) => acc + s + (values[i] ?? ''), '');
        executedQueries.push(query);
        return Promise.resolve([]);
      },
      {
        json: (v: unknown) => v,
        unsafe: async (q: string) => { executedQueries.push(q); return []; },
      },
    ) as unknown as Sql;

    mockConfig = {
      NODE_ENV: 'production',
      PORT: 9503,
      HOST: '127.0.0.1',
      DATABASE_URL: 'postgres://test',
      LOG_LEVEL: 'info',
      RETENTION_RAW_HOURS: 48,
      RETENTION_ROLLUP_DAYS: 90,
      CAPTURE_SIZE_KB: 256,
      CAPTURE_BUDGET_MB: 50,
    } as unknown as Config;
  });

  it('processes modelStatus SSE event and emits delta with seq=1', async () => {
    const fleet = createFleetState();
    const emitter = createDeltaEmitter();
    const deltas: unknown[] = [];
    emitter.subscribe((d) => deltas.push(d));

    const event: LlamaSweepSSEEvent = {
      type: 'modelStatus',
      data: [apiModel('llama3', 'ready')],
    };

    await handleLlamaSweepEvent(fleet, mockSql, mockConfig, 'host1', emitter, event);

    // Assert: delta was emitted
    expect(deltas).toHaveLength(1);
    const delta = deltas[0] as { type: string; seq: number; hosts: Array<{ seq: number; models: Array<{ model: string; state: string }> }> };
    expect(delta.type).toBe('control_fleet');
    expect(delta.seq).toBe(1);
    expect(delta.hosts[0].seq).toBe(1);
    expect(delta.hosts[0].models[0].model).toBe('llama3');
    expect(delta.hosts[0].models[0].state).toBe('ready');

    // Assert: SQL INSERT was called
    expect(executedQueries.length).toBe(1);
    expect(executedQueries[0]).toContain('control_model_events');
    expect(executedQueries[0]).toContain('llama3');
  });

  it('increments seq monotonically across multiple events', async () => {
    const fleet = createFleetState();
    const emitter = createDeltaEmitter();
    const deltas: unknown[] = [];
    emitter.subscribe((d) => deltas.push(d));

    for (let i = 0; i < 3; i++) {
      // Each snapshot adds a new model -> a transition -> a delta.
      await handleLlamaSweepEvent(fleet, mockSql, mockConfig, 'host1', emitter, {
        type: 'modelStatus',
        data: [apiModel(`model${i}`, 'ready')],
      });
    }

    expect(deltas).toHaveLength(3);
    const seqs = deltas.map((d) => (d as { seq: number }).seq);
    expect(seqs).toEqual([1, 2, 3]);
  });

  it('processes metrics event with multiple entries and emits activity deltas', async () => {
    const fleet = createFleetState();
    const emitter = createDeltaEmitter();
    const deltas: unknown[] = [];
    emitter.subscribe((d) => deltas.push(d));

    const metricsEvent: LlamaSweepSSEEvent = {
      type: 'metrics',
      data: [
          {
            id: 1,
            timestamp: '2024-01-01T00:00:00Z',
            model: 'llama3',
            req_path: '/v1/chat/completions',
            resp_status_code: 200,
            duration_ms: 1500,
            tokens: {
              cache_tokens: 100,
              input_tokens: 50,
              output_tokens: 200,
              prompt_per_second: 30,
              tokens_per_second: 50,
            },
            has_capture: false,
          },
          {
            id: 2,
            timestamp: '2024-01-01T00:01:00Z',
            model: 'llama3',
            req_path: '/v1/chat/completions',
            resp_status_code: 200,
            duration_ms: 1200,
            tokens: {
              cache_tokens: 0,
              input_tokens: 100,
              output_tokens: 300,
              prompt_per_second: 25,
              tokens_per_second: 45,
            },
            has_capture: false,
          },
      ],
    };

    await handleLlamaSweepEvent(fleet, mockSql, mockConfig, 'host1', emitter, metricsEvent);

    // handleReconcile is called (gap detection), then 2 activity deltas
    // The reconcile SQL call + 2 INSERT calls = 3 queries
    expect(executedQueries.length).toBeGreaterThanOrEqual(2);

    // Activity deltas (2 entries)
    const activityDeltas = deltas.filter((d) => (d as { type: string }).type === 'control_activity');
    expect(activityDeltas).toHaveLength(2);

    const d1 = activityDeltas[0] as { entry: { id: number } };
    const d2 = activityDeltas[1] as { entry: { id: number } };
    expect(d1.entry.id).toBe(1);
    expect(d2.entry.id).toBe(2);
  });

  it('snapshot seq is max of all host seqs', () => {
    const fleet = createFleetState();

    const host1 = ensureHostState(fleet, 'host1');
    incrementSeq(host1);
    incrementSeq(host1);

    const host2 = ensureHostState(fleet, 'host2');
    incrementSeq(host2);
    incrementSeq(host2);
    incrementSeq(host2);

    const hosts = Array.from(fleet.hosts.values()).map((h) => ({
      providerId: h.providerId,
      seq: h.seq,
    }));
    const snapshotMaxSeq = hosts.reduce((max: number, h: { seq: number }) => Math.max(max, h.seq), 0);
    expect(snapshotMaxSeq).toBe(3);
  });
});

// ─── 2-host delta merge test (B9) ────────────────────────────────────────────

// ─── P4: source column mapping ──────────────────────────────────────────────

describe('P4: source column in metrics ingest', () => {
  let mockSql: Sql;
  let mockConfig: Config;
  let executedQueries: string[];

  beforeEach(() => {
    executedQueries = [];
    mockSql = Object.assign(
      (strings: TemplateStringsArray, ...values: unknown[]) => {
        const query = strings.reduce((acc: string, s: string, i: number) => acc + s + (values[i] ?? ''), '');
        executedQueries.push(query);
        return Promise.resolve([]);
      },
      {
        json: (v: unknown) => v,
        unsafe: async (q: string) => { executedQueries.push(q); return []; },
      },
    ) as unknown as Sql;

    mockConfig = {
      NODE_ENV: 'production',
      PORT: 9503,
      HOST: '127.0.0.1',
      DATABASE_URL: 'postgres://test',
      LOG_LEVEL: 'info',
      RETENTION_RAW_HOURS: 48,
      RETENTION_ROLLUP_DAYS: 90,
      CAPTURE_SIZE_KB: 256,
      CAPTURE_BUDGET_MB: 50,
    } as unknown as Config;
  });

  it('maps source as NULL for ring data (ActivityLogEntry has no headers)', async () => {
    const fleet = createFleetState();
    const emitter = createDeltaEmitter();
    const deltas: unknown[] = [];
    emitter.subscribe((d) => deltas.push(d));

    const metricsEvent: LlamaSweepSSEEvent = {
      type: 'metrics',
      data: [
        {
          id: 1,
          timestamp: '2024-01-01T00:00:00Z',
          model: 'llama3',
          req_path: '/v1/chat/completions',
          resp_status_code: 200,
          duration_ms: 1500,
          tokens: {
            cache_tokens: 100,
            input_tokens: 50,
            output_tokens: 200,
            prompt_per_second: 30,
            tokens_per_second: 50,
          },
          has_capture: false,
        },
      ],
    };

    await handleLlamaSweepEvent(fleet, mockSql, mockConfig, 'host1', emitter, metricsEvent);

    // The INSERT query should include the source column
    const insertQueries = executedQueries.filter((q) => q.includes('control_requests'));
    expect(insertQueries.length).toBeGreaterThanOrEqual(2);
    // The SSE handler INSERT (second one) includes source; reconcile INSERT (first) does not
    expect(insertQueries[1]).toContain('source');
  });
});

describe('2-host delta merge (B9)', () => {
  it('delta for host2 does not wipe host1 from the hosts array', () => {
    // Simulate the merge logic from useControlStream.tsx
    const hosts = [
      { providerId: 'host1', liveness: 'connected' as const, lastSeenAt: '', seq: 5, models: [] },
      { providerId: 'host2', liveness: 'connected' as const, lastSeenAt: '', seq: 3, models: [] },
    ];

    // Delta arrives for host2 only
    const deltaHosts = [
      { providerId: 'host2', liveness: 'connected' as const, lastSeenAt: '', seq: 4, models: [] },
    ];

    const merged = [...hosts];
    for (const dh of deltaHosts) {
      const idx = merged.findIndex((h) => h.providerId === dh.providerId);
      if (idx >= 0) {
        merged[idx] = dh;
      } else {
        merged.push(dh);
      }
    }

    expect(merged).toHaveLength(2);
    expect(merged.find((h) => h.providerId === 'host1')).toBeDefined();
    expect(merged.find((h) => h.providerId === 'host2')!.seq).toBe(4);
    expect(merged.find((h) => h.providerId === 'host1')!.seq).toBe(5);
  });

  it('new host is appended when not in existing array', () => {
    const hosts = [
      { providerId: 'host1', liveness: 'connected' as const, lastSeenAt: '', seq: 5, models: [] },
    ];

    const deltaHosts = [
      { providerId: 'host3', liveness: 'connected' as const, lastSeenAt: '', seq: 1, models: [] },
    ];

    const merged = [...hosts];
    for (const dh of deltaHosts) {
      const idx = merged.findIndex((h) => h.providerId === dh.providerId);
      if (idx >= 0) {
        merged[idx] = dh;
      } else {
        merged.push(dh);
      }
    }

    expect(merged).toHaveLength(2);
    expect(merged.map((h) => h.providerId)).toEqual(['host1', 'host3']);
  });
});
