import assert from 'node:assert/strict';
import test from 'node:test';
import type { XiaobaoVersionRisk } from './xiaobao-risk';
import type { XiaobaoRiskSnapshot } from './xiaobao-risk-trend';
import {
  buildRiskInsightSignature,
  buildRiskInterpretRequest,
  findLatestRiskInsightForVersion,
  findPreviousRiskSnapshot,
  getReusableInsight,
  shouldRequestRiskInsight,
  shouldRequestRiskInsightWithRequestGate,
  shouldRequestRiskInsightWithCacheGate,
  shouldRequestRiskInsightWithCooldown,
} from './xiaobao-risk-ai';
import type { XiaobaoRiskInsightCacheItem } from './xiaobao-risk-cache';

function snapshot(patch: Partial<XiaobaoRiskSnapshot> = {}): XiaobaoRiskSnapshot {
  return {
    versionId: 'ver-1',
    date: '2026-06-28',
    riskScore: 35,
    riskLevel: 'attention',
    forecastReleaseDate: '2026-07-02T10:00:00.000Z',
    openBugCount: 0,
    failedTestCount: 0,
    blockedCount: 0,
    silentRiskCount: 0,
    confidence: 80,
    createdAt: '2026-06-28T10:00:00.000Z',
    ...patch,
  };
}

function risk(patch: Partial<XiaobaoVersionRisk> = {}): XiaobaoVersionRisk {
  return {
    versionId: 'ver-1',
    versionName: 'V1.0',
    productName: 'FTB',
    projectName: 'PM',
    riskScore: 68,
    riskLevel: 'attention',
    expectedReleaseDate: '2026-07-01T10:00:00.000Z',
    confidence: 80,
    confidenceLevel: 'high',
    forecastReleaseDate: '2026-07-02T10:00:00.000Z',
    delayDays: 1,
    remainingWorkHours: 12,
    reasons: [
      { key: 'failed_test', title: 'Failed tests', detail: '1 test failed.', severity: 'warning', count: 1 },
      { key: 'critical_bug', title: 'Critical bug', detail: 'P1 bug exists.', severity: 'danger', count: 1 },
    ],
    silentRisks: [{ key: 'no_update', title: 'No update', detail: 'No update for 5 days.' }],
    dailyEvidence: {
      todayDeliveries: [{ id: 'ev-1', title: 'Delivery', summary: 'Submitted core flow.', occurredAt: '2026-06-29T09:00:00.000Z' }],
      todayProgress: [{ id: 'ev-2', title: 'Progress', summary: 'Fixed login issue.', occurredAt: '2026-06-29T10:00:00.000Z' }],
      todayCreations: [{ id: 'ev-create', title: 'Creation', summary: 'Created regression case.', occurredAt: '2026-06-29T10:30:00.000Z' }],
      todayRisks: [{ id: 'ev-3', title: 'Risk', summary: 'Regression failed.', occurredAt: '2026-06-29T11:00:00.000Z' }],
      progressNotes: [{ id: 'ev-4', title: 'Note', summary: 'Need QA retest.', occurredAt: '2026-06-29T12:00:00.000Z' }],
      needsProgressItems: [{ id: 'ev-5', title: 'Need update', summary: 'Backend task has no update.', occurredAt: '2026-06-29T13:00:00.000Z' }],
      recentActivityCount: 3,
      totalActivityCount: 6,
      todayActualHours: 4.5,
      lastActivityAt: '2026-06-29T13:00:00.000Z',
    },
    signals: {
      unfinishedCount: 4,
      openBugCount: 3,
      criticalBugCount: 1,
      failedTestCount: 1,
      blockedCount: 0,
      silentRiskCount: 1,
      daysToExpectedRelease: 1,
    },
    currentSnapshot: snapshot({ riskScore: 68, openBugCount: 3, failedTestCount: 1, silentRiskCount: 1 }),
    trend: { direction: 'up', delta: 33, summary: 'Risk rose by 33 points.', pattern: 'score_delta' },
    ...patch,
  } as unknown as XiaobaoVersionRisk;
}

function insight(patch: Partial<XiaobaoRiskInsightCacheItem> = {}): XiaobaoRiskInsightCacheItem {
  return {
    versionId: 'ver-1',
    riskSignature: buildRiskInsightSignature(risk({ riskLevel: 'attention', riskScore: 52 })),
    insight: {
      summary: 'Risk needs attention.',
      why: ['Risk rose.'],
      forecast: 'May slip.',
      suggestedActions: ['Confirm scope.'],
      ownerHints: ['PM'],
      generatedAt: '2026-06-29T08:00:00.000Z',
    },
    generatedAt: '2026-06-29T08:00:00.000Z',
    ...patch,
  };
}

test('shouldRequestRiskInsight skips on_track', () => {
  assert.equal(shouldRequestRiskInsight(risk({ riskLevel: 'on_track', riskScore: 10 }), undefined), false);
});

test('shouldRequestRiskInsight does not trigger ordinary attention without a previous worsening signal', () => {
  assert.equal(shouldRequestRiskInsight(risk({ riskLevel: 'attention', riskScore: 42, signals: { ...risk().signals, unfinishedCount: 0, daysToExpectedRelease: 5 } }), undefined), false);
});

test('shouldRequestRiskInsight triggers attention when risk score jumps', () => {
  assert.equal(shouldRequestRiskInsight(risk({ riskLevel: 'attention', riskScore: 68 }), snapshot({ riskScore: 35 })), true);
});

test('shouldRequestRiskInsight triggers attention when risk signals worsen without level change', () => {
  assert.equal(
    shouldRequestRiskInsight(
      risk({
        riskLevel: 'attention',
        riskScore: 52,
        signals: {
          unfinishedCount: 4,
          openBugCount: 3,
          criticalBugCount: 3,
          failedTestCount: 1,
          blockedCount: 1,
          silentRiskCount: 1,
          daysToExpectedRelease: 1,
        },
      }),
      snapshot({ riskScore: 50, openBugCount: 0, failedTestCount: 0, blockedCount: 0, silentRiskCount: 0 }),
    ),
    true,
  );
});

test('shouldRequestRiskInsight triggers attention when critical bugs increase even if open bug total is unchanged', () => {
  assert.equal(
    shouldRequestRiskInsight(
      risk({
        riskLevel: 'attention',
        riskScore: 45,
        signals: {
          unfinishedCount: 0,
          openBugCount: 2,
          criticalBugCount: 1,
          failedTestCount: 0,
          blockedCount: 0,
          silentRiskCount: 0,
          daysToExpectedRelease: 5,
        },
      }),
      snapshot({ riskScore: 44, openBugCount: 2, criticalBugCount: 0 } as Partial<XiaobaoRiskSnapshot>),
    ),
    true,
  );
});

test('shouldRequestRiskInsight triggers attention on continuously rising trend', () => {
  assert.equal(
    shouldRequestRiskInsight(
      risk({
        riskLevel: 'attention',
        riskScore: 46,
        signals: {
          unfinishedCount: 0,
          openBugCount: 0,
          criticalBugCount: 0,
          failedTestCount: 0,
          blockedCount: 0,
          silentRiskCount: 0,
          daysToExpectedRelease: 5,
        },
        trend: { direction: 'up', delta: 16, summary: 'Risk rose continuously from 30 to 46.', pattern: 'continuous_rising' },
      }),
      snapshot({ riskScore: 38 }),
    ),
    true,
  );
});

test('shouldRequestRiskInsight triggers attention on continuously rising trend without previous snapshot', () => {
  assert.equal(
    shouldRequestRiskInsight(
      risk({
        riskLevel: 'attention',
        riskScore: 46,
        signals: {
          unfinishedCount: 0,
          openBugCount: 0,
          criticalBugCount: 0,
          failedTestCount: 0,
          blockedCount: 0,
          silentRiskCount: 0,
          daysToExpectedRelease: 5,
        },
        trend: { direction: 'up', delta: 16, summary: 'Risk rose continuously from 30 to 46.', pattern: 'continuous_rising' },
      }),
      undefined,
    ),
    true,
  );
});

test('shouldRequestRiskInsight triggers attention near release with unfinished work even without history', () => {
  assert.equal(
    shouldRequestRiskInsight(risk({ riskLevel: 'attention', signals: { ...risk().signals, unfinishedCount: 2, daysToExpectedRelease: 1 } }), undefined),
    true,
  );
});

test('shouldRequestRiskInsight triggers high risk levels', () => {
  assert.equal(shouldRequestRiskInsight(risk({ riskLevel: 'at_risk' }), undefined), true);
  assert.equal(shouldRequestRiskInsight(risk({ riskLevel: 'likely_delayed' }), undefined), true);
  assert.equal(shouldRequestRiskInsight(risk({ riskLevel: 'blocked' }), undefined), true);
});

test('findLatestRiskInsightForVersion returns latest generated insight for a version', () => {
  const latest = findLatestRiskInsightForVersion([
    insight({ versionId: 'ver-1', generatedAt: '2026-06-29T08:00:00.000Z' }),
    insight({ versionId: 'ver-1', generatedAt: '2026-06-29T10:00:00.000Z' }),
    insight({ versionId: 'ver-2', generatedAt: '2026-06-29T11:00:00.000Z' }),
  ], 'ver-1');

  assert.equal(latest?.generatedAt, '2026-06-29T10:00:00.000Z');
});

test('shouldRequestRiskInsightWithCooldown skips repeated AI requests inside cooldown', () => {
  const current = risk({ riskLevel: 'at_risk', riskScore: 62 });
  const previous = snapshot({ riskScore: 40 });
  const latest = insight({
    generatedAt: '2026-06-29T10:00:00.000Z',
    riskSignature: buildRiskInsightSignature(risk({ riskLevel: 'at_risk', riskScore: 60 })),
  });

  assert.equal(shouldRequestRiskInsightWithCooldown(current, previous, latest, new Date('2026-06-29T11:00:00.000Z')), false);
});

test('shouldRequestRiskInsightWithCooldown allows AI requests after cooldown', () => {
  const current = risk({ riskLevel: 'at_risk', riskScore: 62 });
  const previous = snapshot({ riskScore: 40 });
  const latest = insight({ generatedAt: '2026-06-29T04:00:00.000Z' });

  assert.equal(shouldRequestRiskInsightWithCooldown(current, previous, latest, new Date('2026-06-29T11:00:00.000Z')), true);
});

test('shouldRequestRiskInsightWithCooldown bypasses cooldown when risk level escalates', () => {
  const previousInsight = insight({
    generatedAt: '2026-06-29T10:30:00.000Z',
    riskSignature: buildRiskInsightSignature(risk({ riskLevel: 'attention', riskScore: 52 })),
  });
  const current = risk({ riskLevel: 'blocked', riskScore: 90 });

  assert.equal(shouldRequestRiskInsightWithCooldown(current, snapshot({ riskScore: 50 }), previousInsight, new Date('2026-06-29T11:00:00.000Z')), true);
});

test('shouldRequestRiskInsightWithCacheGate waits for cache loading before AI requests', () => {
  const current = risk({ riskLevel: 'at_risk', riskScore: 82 });

  assert.equal(
    shouldRequestRiskInsightWithCacheGate(false, [], current, snapshot({ riskScore: 45 }), new Date('2026-06-29T11:00:00.000Z')),
    false,
  );
});

test('shouldRequestRiskInsightWithCacheGate reuses unchanged cached insight after cache loading', () => {
  const current = risk({ riskLevel: 'at_risk', riskScore: 82 });
  const cached = insight({
    riskSignature: buildRiskInsightSignature(current),
    generatedAt: '2026-06-29T02:00:00.000Z',
  });

  assert.equal(
    shouldRequestRiskInsightWithCacheGate(true, [cached], current, snapshot({ riskScore: 45 }), new Date('2026-06-29T11:00:00.000Z')),
    false,
  );
});

test('shouldRequestRiskInsightWithCacheGate allows changed risk facts after cache loading and cooldown', () => {
  const current = risk({ riskLevel: 'at_risk', riskScore: 82 });
  const previousInsight = insight({
    riskSignature: buildRiskInsightSignature(risk({ riskLevel: 'at_risk', riskScore: 62 })),
    generatedAt: '2026-06-29T02:00:00.000Z',
  });

  assert.equal(
    shouldRequestRiskInsightWithCacheGate(true, [previousInsight], current, snapshot({ riskScore: 45 }), new Date('2026-06-29T11:00:00.000Z')),
    true,
  );
});

test('shouldRequestRiskInsightWithRequestGate skips repeat requests during request cooldown without cache', () => {
  const current = risk({ riskLevel: 'at_risk', riskScore: 82 });

  assert.equal(
    shouldRequestRiskInsightWithRequestGate({
      riskCacheLoaded: true,
      cache: [],
      current,
      previous: snapshot({ riskScore: 45 }),
      lastRequestedAt: '2026-06-29T10:30:00.000Z',
      now: new Date('2026-06-29T11:00:00.000Z'),
    }),
    false,
  );
});

test('buildRiskInsightSignature uses all open bugs, not only critical bugs', () => {
  const base = buildRiskInsightSignature(risk({
    signals: { ...risk().signals, openBugCount: 1, criticalBugCount: 0 },
    currentSnapshot: snapshot({ openBugCount: 1, riskScore: 45 }),
  }));
  const changed = buildRiskInsightSignature(risk({
    signals: { ...risk().signals, openBugCount: 2, criticalBugCount: 0 },
    currentSnapshot: snapshot({ openBugCount: 2, riskScore: 45 }),
  }));

  assert.notEqual(base, changed);
});

test('buildRiskInsightSignature changes when trend or evidence changes', () => {
  const base = buildRiskInsightSignature(risk({
    trend: { direction: 'flat', delta: 0, summary: 'Risk is stable.', pattern: 'stable' },
  }));
  const changed = buildRiskInsightSignature(risk({
    trend: { direction: 'up', delta: 16, summary: 'Risk rose continuously from 30 to 46.', pattern: 'continuous_rising' },
    dailyEvidence: {
      ...risk().dailyEvidence!,
      todayRisks: [{ id: 'ev-new', title: 'Risk', summary: 'New P1 regression appeared.', occurredAt: '2026-06-29T15:00:00.000Z' }],
    },
  }));

  assert.notEqual(base, changed);
});

test('buildRiskInsightSignature changes when trend direction delta or activity metadata changes', () => {
  const base = buildRiskInsightSignature(risk({
    trend: { direction: 'up', delta: 10, summary: 'Risk changed.', pattern: 'score_delta' },
    dailyEvidence: {
      ...risk().dailyEvidence!,
      recentActivityCount: 1,
      lastActivityAt: '2026-06-29T10:00:00.000Z',
    },
  }));
  const changed = buildRiskInsightSignature(risk({
    trend: { direction: 'down', delta: -10, summary: 'Risk changed.', pattern: 'score_delta' },
    dailyEvidence: {
      ...risk().dailyEvidence!,
      recentActivityCount: 2,
      lastActivityAt: '2026-06-29T11:00:00.000Z',
    },
  }));

  assert.notEqual(base, changed);
});

test('buildRiskInsightSignature stays stable when only volatile same-day forecast timing changes', () => {
  const base = buildRiskInsightSignature(risk({
    forecastReleaseDate: '2026-07-14T03:06:58.211Z',
    signals: { ...risk().signals, daysToExpectedRelease: 30.9 },
  }));
  const changed = buildRiskInsightSignature(risk({
    forecastReleaseDate: '2026-07-14T03:37:15.903Z',
    signals: { ...risk().signals, daysToExpectedRelease: 30.1 },
  }));

  assert.equal(base, changed);
});

test('getReusableInsight reuses legacy signatures with volatile forecast timestamps', () => {
  const current = risk({
    forecastReleaseDate: '2026-07-14T03:37:15.903Z',
    signals: { ...risk().signals, daysToExpectedRelease: 30.1 },
  });
  const legacy = insight({
    riskSignature: JSON.stringify({
      ...JSON.parse(buildRiskInsightSignature(current)),
      forecastReleaseDate: '2026-07-14T03:06:58.211Z',
      signals: {
        ...current.signals,
        daysToExpectedRelease: 30.9,
      },
    }),
  });

  assert.equal(getReusableInsight([legacy], current)?.insight.summary, legacy.insight.summary);
});

test('getReusableInsight keeps showing cached insight when only refresh-volatile fields drift', () => {
  const cachedRisk = risk({
    riskLevel: 'likely_delayed',
    riskScore: 100,
    confidence: 65,
    forecastReleaseDate: '2026-07-14T03:37:15.903Z',
    signals: { ...risk().signals, failedTestCount: 14, silentRiskCount: 58, daysToExpectedRelease: 1 },
    dailyEvidence: {
      ...risk().dailyEvidence!,
      recentActivityCount: 12,
      todayActualHours: 1.5,
      lastActivityAt: '2026-06-29T03:37:15.903Z',
      todayProgress: [
        { id: 'ev-2', title: 'Progress', summary: 'Fixed login issue.', occurredAt: '2026-06-29T03:37:15.903Z' },
      ],
    },
    currentSnapshot: snapshot({ riskScore: 100, failedTestCount: 14, silentRiskCount: 58 }),
  });
  const current = risk({
    riskLevel: 'likely_delayed',
    riskScore: 100,
    confidence: 65,
    forecastReleaseDate: '2026-07-15T06:21:38.597Z',
    signals: { ...risk().signals, failedTestCount: 14, silentRiskCount: 58, daysToExpectedRelease: 0 },
    dailyEvidence: {
      ...risk().dailyEvidence!,
      recentActivityCount: 13,
      todayActualHours: 3,
      lastActivityAt: '2026-06-29T06:21:38.597Z',
      todayProgress: [
        { id: 'ev-2', title: 'Progress', summary: 'Fixed login issue.', occurredAt: '2026-06-29T06:21:38.597Z' },
      ],
    },
    currentSnapshot: snapshot({ riskScore: 100, failedTestCount: 14, silentRiskCount: 58 }),
  });
  const cached = insight({
    riskSignature: buildRiskInsightSignature(cachedRisk),
    insight: { ...insight().insight, summary: 'Cached Xiaobao advice stays visible.' },
    generatedAt: '2026-06-29T08:00:00.000Z',
  });

  assert.equal(getReusableInsight([cached], current)?.insight.summary, 'Cached Xiaobao advice stays visible.');
  assert.equal(
    shouldRequestRiskInsightWithCacheGate(true, [cached], current, snapshot({ riskScore: 70 }), new Date('2026-06-29T09:00:00.000Z')),
    false,
  );
});

test('getReusableInsight keeps cached insight when only daily report evidence details change', () => {
  const cachedRisk = risk({
    riskLevel: 'at_risk',
    riskScore: 82,
    confidence: 70,
    signals: { ...risk().signals, failedTestCount: 2, silentRiskCount: 1, daysToExpectedRelease: 1 },
    dailyEvidence: {
      todayDeliveries: [],
      todayProgress: [
        { id: 'ev-old', title: 'Progress', summary: 'Worked on checkout flow.', occurredAt: '2026-06-30T03:00:00.000Z' },
      ],
      todayCreations: [],
      todayRisks: [],
      progressNotes: [],
      needsProgressItems: [],
      recentActivityCount: 2,
      totalActivityCount: 1,
      todayActualHours: 1,
      lastActivityAt: '2026-06-30T03:00:00.000Z',
    },
    currentSnapshot: snapshot({ riskScore: 82, failedTestCount: 2, silentRiskCount: 1 }),
  });
  const current = risk({
    riskLevel: 'at_risk',
    riskScore: 82,
    confidence: 70,
    signals: { ...risk().signals, failedTestCount: 2, silentRiskCount: 1, daysToExpectedRelease: 1 },
    dailyEvidence: {
      todayDeliveries: [],
      todayProgress: [
        { id: 'ev-new', title: 'Progress', summary: 'Updated checkout flow and added handoff notes.', occurredAt: '2026-07-01T04:00:00.000Z' },
        { id: 'ev-new-2', title: 'Progress', summary: 'Confirmed QA scope.', occurredAt: '2026-07-01T05:00:00.000Z' },
      ],
      todayCreations: [],
      todayRisks: [],
      progressNotes: [
        { id: 'note-new', title: 'Note', summary: 'Next work starts tomorrow morning.', occurredAt: '2026-07-01T06:00:00.000Z' },
      ],
      needsProgressItems: [],
      recentActivityCount: 6,
      totalActivityCount: 3,
      todayActualHours: 4,
      lastActivityAt: '2026-07-01T06:00:00.000Z',
    },
    currentSnapshot: snapshot({ riskScore: 82, failedTestCount: 2, silentRiskCount: 1 }),
  });
  const cached = insight({
    riskSignature: buildRiskInsightSignature(cachedRisk),
    insight: { ...insight().insight, summary: 'Cached advice should remain visible.' },
    generatedAt: '2026-06-30T08:00:00.000Z',
  });

  assert.equal(getReusableInsight([cached], current)?.insight.summary, 'Cached advice should remain visible.');
  assert.equal(
    shouldRequestRiskInsightWithCacheGate(true, [cached], current, snapshot({ riskScore: 70 }), new Date('2026-07-01T09:00:00.000Z')),
    false,
  );
});

test('getReusableInsight does not reuse cached insight when core risk facts change', () => {
  const cachedRisk = risk({
    riskLevel: 'likely_delayed',
    riskScore: 100,
    confidence: 65,
    forecastReleaseDate: '2026-07-14T03:37:15.903Z',
    signals: { ...risk().signals, failedTestCount: 14, silentRiskCount: 58, daysToExpectedRelease: 1 },
    currentSnapshot: snapshot({ riskScore: 100, failedTestCount: 14, silentRiskCount: 58 }),
  });
  const current = risk({
    riskLevel: 'likely_delayed',
    riskScore: 100,
    confidence: 65,
    forecastReleaseDate: '2026-07-15T06:21:38.597Z',
    signals: { ...risk().signals, failedTestCount: 15, silentRiskCount: 58, daysToExpectedRelease: 0 },
    currentSnapshot: snapshot({ riskScore: 100, failedTestCount: 15, silentRiskCount: 58 }),
  });
  const cached = insight({ riskSignature: buildRiskInsightSignature(cachedRisk) });

  assert.equal(getReusableInsight([cached], current), undefined);
});

test('buildRiskInterpretRequest compresses frontend risk evidence for the backend AI contract', () => {
  const payload = buildRiskInterpretRequest(risk());

  assert.equal(payload.versionName, 'V1.0');
  assert.equal(payload.reasons[0].label, 'Failed tests');
  assert.equal(payload.reasons[0].severity, 'medium');
  assert.equal(payload.reasons[1].severity, 'critical');
  assert.deepEqual(payload.dailyEvidence.todayDeliveries, ['Submitted core flow.']);
  assert.deepEqual(payload.dailyEvidence.todayCreations, ['Created regression case.']);
  assert.deepEqual(payload.dailyEvidence.needsProgressItems, ['Backend task has no update.']);
  assert.equal(payload.dailyEvidence.totalActivityCount, 6);
  assert.equal(payload.dailyEvidence.todayActualHours, 4.5);
  assert.equal(payload.silentRisks[0].detail, 'No update for 5 days.');
});

test('findPreviousRiskSnapshot returns latest snapshot before today for the version', () => {
  const previous = findPreviousRiskSnapshot(
    [
      snapshot({ versionId: 'ver-1', date: '2026-06-29', riskScore: 70, createdAt: '2026-06-29T10:00:00.000Z' }),
      snapshot({ versionId: 'ver-1', date: '2026-06-28', riskScore: 52, createdAt: '2026-06-28T10:00:00.000Z' }),
      snapshot({ versionId: 'ver-1', date: '2026-06-27', riskScore: 38, createdAt: '2026-06-27T10:00:00.000Z' }),
      snapshot({ versionId: 'ver-2', date: '2026-06-28', riskScore: 90, createdAt: '2026-06-28T11:00:00.000Z' }),
    ],
    'ver-1',
    '2026-06-29',
  );

  assert.equal(previous?.riskScore, 52);
});
