import { describe, it, expect } from 'vitest';
import { aggregateStats } from '@main/services/telemetryStats.js';
import type { TelemetryEvent } from '@main/services/telemetryEvents.js';

const e = (ts: string, kind: TelemetryEvent['kind'], payload: TelemetryEvent['payload']): TelemetryEvent =>
  ({ ts, kind, payload } as TelemetryEvent);

describe('aggregateStats', () => {
  it('produces empty stats for empty input', () => {
    const r = aggregateStats([], new Date('2026-05-08T00:00:00Z'));
    expect(r.eventCount).toBe(0);
    expect(r.md).toContain('총 이벤트: 0');
  });

  it('counts events per KST day per kind', () => {
    const events: TelemetryEvent[] = [
      e('2026-05-01T12:00:00Z', 'capture', { noteId: 'n1', rawTextLength: 5, hasMedia: false }),
      e('2026-05-01T12:01:00Z', 'capture', { noteId: 'n2', rawTextLength: 3, hasMedia: true }),
      e('2026-05-01T12:02:00Z', 'ai_succeeded', { noteId: 'n1', durationMs: 1000, attempts: 0 }),
      e('2026-05-02T00:00:00Z', 'ai_failed', { noteId: 'n2', reason: 'unreachable', attempts: 3 })
    ];
    const r = aggregateStats(events, new Date('2026-05-08T00:00:00Z'));
    expect(r.eventCount).toBe(4);
    expect(r.md).toContain('| 2026-05-01 | 2 | 1 | 0 |');
    expect(r.md).toContain('| 2026-05-02 | 0 | 0 | 1 |');
  });

  it('computes AI 성공률', () => {
    const events: TelemetryEvent[] = [
      e('2026-05-01T00:00:00Z', 'ai_succeeded', { noteId: 'n1', durationMs: 1, attempts: 0 }),
      e('2026-05-01T00:00:01Z', 'ai_succeeded', { noteId: 'n2', durationMs: 1, attempts: 0 }),
      e('2026-05-01T00:00:02Z', 'ai_succeeded', { noteId: 'n3', durationMs: 1, attempts: 0 }),
      e('2026-05-01T00:00:03Z', 'ai_failed', { noteId: 'n4', reason: 'other', attempts: 1 })
    ];
    const r = aggregateStats(events, new Date('2026-05-02T00:00:00Z'));
    expect(r.md).toContain('AI 성공률: 75.0%');
    expect(r.md).toContain('3/4');
  });

  it('AI 성공률 N/A when no AI events', () => {
    const events: TelemetryEvent[] = [
      e('2026-05-01T00:00:00Z', 'capture', { noteId: 'n1', rawTextLength: 1, hasMedia: false })
    ];
    const r = aggregateStats(events, new Date('2026-05-02T00:00:00Z'));
    expect(r.md).toContain('AI 성공률: N/A');
  });

  it('computes 평균 ai_succeeded durationMs', () => {
    const events: TelemetryEvent[] = [
      e('2026-05-01T00:00:00Z', 'ai_succeeded', { noteId: 'n1', durationMs: 1000, attempts: 0 }),
      e('2026-05-01T00:00:01Z', 'ai_succeeded', { noteId: 'n2', durationMs: 2000, attempts: 0 }),
      e('2026-05-01T00:00:02Z', 'ai_succeeded', { noteId: 'n3', durationMs: 3000, attempts: 0 })
    ];
    const r = aggregateStats(events, new Date('2026-05-02T00:00:00Z'));
    expect(r.md).toContain('평균 ai_succeeded durationMs: 2000');
  });

  it('buckets near-midnight UTC events on the correct KST day (regression: not naive UTC)', () => {
    // 2026-05-01T15:30:00Z → 2026-05-02 00:30 KST → KST day 2026-05-02
    // Naive UTC slice(0,10) would put this on 2026-05-01 — this test catches that regression.
    const events: TelemetryEvent[] = [
      e('2026-05-01T15:30:00Z', 'capture', { noteId: 'n1', rawTextLength: 1, hasMedia: false })
    ];
    const r = aggregateStats(events, new Date('2026-05-08T00:00:00Z'));
    expect(r.md).toContain('| 2026-05-02 | 1 | 0 | 0 |');
    expect(r.md).not.toContain('| 2026-05-01 |');
  });
});

describe('aggregateStats — trash family (v0.2.3 #4)', () => {
  it('counts trash/restore/permanent_delete/empty_trash per day', () => {
    const events: TelemetryEvent[] = [
      e('2026-05-01T00:00:00Z', 'trash', { noteId: 'n1' }),
      e('2026-05-01T01:00:00Z', 'trash', { noteId: 'n2' }),
      e('2026-05-01T02:00:00Z', 'restore', { noteId: 'n1' }),
      e('2026-05-01T03:00:00Z', 'permanent_delete', { noteId: 'n3' }),
      e('2026-05-01T04:00:00Z', 'empty_trash', { count: 5 })
    ];
    const r = aggregateStats(events, new Date('2026-05-08T00:00:00Z'));
    expect(r.eventCount).toBe(5);
    expect(r.md).toContain('| 2026-05-01 | 0 | 0 | 0 | 2 | 1 | 1 | 1 |');
  });

  it('computes restore/trash ratio', () => {
    const events: TelemetryEvent[] = [
      e('2026-05-01T00:00:00Z', 'trash', { noteId: 'a' }),
      e('2026-05-01T00:00:01Z', 'trash', { noteId: 'b' }),
      e('2026-05-01T00:00:02Z', 'trash', { noteId: 'c' }),
      e('2026-05-01T00:00:03Z', 'trash', { noteId: 'd' }),
      e('2026-05-01T00:00:04Z', 'restore', { noteId: 'a' })
    ];
    const r = aggregateStats(events, new Date('2026-05-02T00:00:00Z'));
    expect(r.md).toContain('휴지통 회수율: 25.0% (1/4)');
  });

  it('휴지통 회수율 N/A when no trash events', () => {
    const events: TelemetryEvent[] = [
      e('2026-05-01T00:00:00Z', 'capture', { noteId: 'n1', rawTextLength: 1, hasMedia: false })
    ];
    const r = aggregateStats(events, new Date('2026-05-02T00:00:00Z'));
    expect(r.md).toContain('휴지통 회수율: N/A');
  });
});

describe('aggregateStats — expired_banner_shown / expired_batch_trash', () => {
  it('counts both kinds per day and computes 만료 trash ratio', () => {
    const events = [
      { ts: '2026-05-01T00:00:00.000Z', kind: 'expired_banner_shown' as const, payload: { candidateCount: 5 } },
      { ts: '2026-05-01T01:00:00.000Z', kind: 'expired_banner_shown' as const, payload: { candidateCount: 3 } },
      { ts: '2026-05-01T02:00:00.000Z', kind: 'expired_batch_trash' as const, payload: { count: 4 } }
    ];
    const r = aggregateStats(events, new Date('2026-05-02T00:00:00Z'));
    expect(r.md).toContain('expired_banner_shown');
    expect(r.md).toContain('expired_batch_trash');
    // 4 / (5 + 3) = 50.0%
    expect(r.md).toMatch(/만료 trash ratio.*50\.0%/);
  });

  it('shows N/A when 만료 배너 노출 0건', () => {
    const events = [
      { ts: '2026-05-01T00:00:00.000Z', kind: 'capture' as const, payload: { noteId: 'a', rawTextLength: 1, hasMedia: false } }
    ];
    const r = aggregateStats(events, new Date('2026-05-02T00:00:00Z'));
    expect(r.md).toMatch(/만료 trash ratio.*N\/A/);
  });
});

describe('aggregateStats — ollama_* events', () => {
  it('counts 3 kinds per day and computes downtime average', () => {
    const events = [
      { ts: '2026-05-01T00:00:00.000Z', kind: 'ollama_unreachable' as const, payload: { reason: 'refused' } },
      { ts: '2026-05-01T01:00:00.000Z', kind: 'ollama_recovered' as const, payload: { downtimeMs: 60000 } },
      { ts: '2026-05-01T02:00:00.000Z', kind: 'ollama_unreachable' as const, payload: { reason: 'timeout' } },
      { ts: '2026-05-01T03:00:00.000Z', kind: 'ollama_recovered' as const, payload: { downtimeMs: 120000 } },
      { ts: '2026-05-01T04:00:00.000Z', kind: 'ollama_recheck_manual' as const, payload: {} as Record<string, never> }
    ];
    const r = aggregateStats(events, new Date('2026-05-02T00:00:00Z'));
    expect(r.md).toContain('ollama_unreachable');
    expect(r.md).toContain('ollama_recovered');
    expect(r.md).toContain('ollama_recheck_manual');
    // (60000 + 120000) / 2 = 90000
    expect(r.md).toMatch(/평균 downtimeMs.*90000/);
    expect(r.md).toMatch(/수동 recheck.*1/);
  });

  it('shows N/A for downtime when no recovered events', () => {
    const events = [
      { ts: '2026-05-01T00:00:00.000Z', kind: 'ollama_unreachable' as const, payload: { reason: 'refused' } }
    ];
    const r = aggregateStats(events, new Date('2026-05-02T00:00:00Z'));
    expect(r.md).toMatch(/평균 downtimeMs.*N\/A/);
  });
});

describe('aggregateStats — ai_retry_manual', () => {
  it('counts events and sums failedCount', () => {
    const events = [
      { ts: '2026-05-01T00:00:00.000Z', kind: 'ai_retry_manual' as const, payload: { failedCount: 3 } },
      { ts: '2026-05-01T01:00:00.000Z', kind: 'ai_retry_manual' as const, payload: { failedCount: 7 } }
    ];
    const r = aggregateStats(events, new Date('2026-05-02T00:00:00Z'));
    expect(r.md).toContain('ai_retry_manual');
    // 2회 / 누적 10건
    expect(r.md).toMatch(/AI 수동 재시도.*2회.*10건/);
  });
});

describe('aggregateStats — tag_vocab hit/miss', () => {
  it('aggregates tag_vocab hit/miss with success rate', () => {
    const events: TelemetryEvent[] = [
      e('2026-05-02T00:00:00Z', 'tag_vocab_hit', { tagId: 1, vocabSize: 10 }),
      e('2026-05-02T00:00:01Z', 'tag_vocab_hit', { tagId: 2, vocabSize: 10 }),
      e('2026-05-02T00:00:02Z', 'tag_vocab_hit', { tagId: 3, vocabSize: 10 }),
      e('2026-05-02T00:00:03Z', 'tag_vocab_hit', { tagId: 4, vocabSize: 10 }),
      e('2026-05-02T00:00:04Z', 'tag_vocab_hit', { tagId: 5, vocabSize: 10 }),
      e('2026-05-02T00:00:05Z', 'tag_vocab_miss', { vocabSize: 10 }),
      e('2026-05-02T00:00:06Z', 'tag_vocab_miss', { vocabSize: 10 }),
      e('2026-05-02T00:00:07Z', 'tag_vocab_miss', { vocabSize: 10 })
    ];
    const r = aggregateStats(events, new Date('2026-05-03T00:00:00Z'));
    expect(r.md).toContain('태그 vocab: hit/miss = 5/3');
    expect(r.md).toContain('적중률 62.5%');
  });

  it('태그 vocab summary shows 데이터 없음 when no events', () => {
    const r = aggregateStats([], new Date('2026-05-03T00:00:00Z'));
    expect(r.md).toContain('태그 vocab');
    expect(r.md).toContain('데이터 없음');
  });

  it('aggregates recall events with open rate + average ageDays', () => {
    const events: TelemetryEvent[] = [
      e('2026-05-02T00:00:00Z', 'recall_shown', { noteId: 'n1', ageDays: 10 }),
      e('2026-05-02T00:00:01Z', 'recall_shown', { noteId: 'n2', ageDays: 20 }),
      e('2026-05-02T00:00:02Z', 'recall_shown', { noteId: 'n3', ageDays: 30 }),
      e('2026-05-02T00:00:03Z', 'recall_shown', { noteId: 'n4', ageDays: 40 }),
      e('2026-05-02T00:00:04Z', 'recall_opened', { noteId: 'n1' }),
      e('2026-05-02T00:00:05Z', 'recall_opened', { noteId: 'n2' }),
      e('2026-05-02T00:00:06Z', 'recall_dismissed', { noteId: 'n3' }),
      e('2026-05-02T00:00:07Z', 'recall_snoozed', { noteId: 'n4' })
    ];
    const r = aggregateStats(events, new Date('2026-05-03T00:00:00Z'));
    expect(r.md).toContain('회상 추천: shown 4 / opened 2 / dismissed 1 / snoozed 1');
    expect(r.md).toContain('열림율 50.0%');
    expect(r.md).toContain('회상 평균 ageDays: 25');  // (10+20+30+40)/4
  });

  it('회상 summary shows 데이터 없음 when no recall events', () => {
    const r = aggregateStats([], new Date('2026-05-03T00:00:00Z'));
    expect(r.md).toContain('회상 추천');
    expect(r.md).toContain('데이터 없음');
  });
});
