import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';

import { describe, expect, it, vi } from 'vitest';

import {
  classifyInstallerObservation,
  installerObservationRoot,
  normalizeUpdateObservationChannel,
  observePendingInstallerApplyAttempts,
  type InstallerObservationSummary,
} from '../src/update-apply-observations.js';

function pendingSummary(overrides: Partial<InstallerObservationSummary> = {}): InstallerObservationSummary {
  return {
    arch: 'x64',
    artifactType: 'installer',
    attemptedAt: '2026-05-20T00:00:00.000Z',
    channel: 'beta',
    flowId: 'flow-1',
    fromVersion: '0.8.0-beta.5',
    kind: 'installer_apply_observation',
    namespace: 'release-beta-win',
    platform: 'win32',
    reason: 'installer_open_requested',
    result: 'pending',
    schemaVersion: 1,
    toVersion: '0.8.0-beta.6',
    updatedAt: '2026-05-20T00:00:00.000Z',
    ...overrides,
  };
}

async function writeSummary(dataRoot: string, summary: InstallerObservationSummary): Promise<string> {
  const filePath = path.join(installerObservationRoot(dataRoot), summary.flowId, 'summary.json');
  await mkdir(path.dirname(filePath), { recursive: true });
  await writeFile(filePath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8');
  return filePath;
}

describe('update apply observations', () => {
  it('classifies next-launch version matches and unchanged versions', () => {
    const now = new Date('2026-05-20T00:03:00.000Z');
    expect(classifyInstallerObservation(pendingSummary(), {
      currentChannel: 'beta',
      currentVersion: '0.8.0-beta.6',
      namespace: 'release-beta-win',
      now,
    })).toMatchObject({
      elapsedBucket: 'lt_5m',
      reason: 'app_version_matches',
      result: 'success',
    });

    expect(classifyInstallerObservation(pendingSummary(), {
      currentChannel: 'beta',
      currentVersion: '0.8.0-beta.5',
      namespace: 'release-beta-win',
      now,
    })).toMatchObject({
      reason: 'app_version_unchanged',
      result: 'not_applied',
    });
  });

  it('classifies mismatched runtime identity without using raw paths or errors', () => {
    expect(classifyInstallerObservation(pendingSummary(), {
      currentChannel: 'stable',
      currentVersion: '0.8.0-beta.6',
      namespace: 'release-beta-win',
      now: new Date('2026-05-20T00:03:00.000Z'),
    })).toMatchObject({
      reason: 'identity_mismatch',
      result: 'unknown',
    });
  });

  it('normalizes nightly and preview observation channels without collapsing them to beta', () => {
    expect(normalizeUpdateObservationChannel('0.8.0.nightly.2')).toBe('nightly');
    expect(normalizeUpdateObservationChannel('0.8.0-preview.2')).toBe('preview');
    expect(normalizeUpdateObservationChannel('0.8.0', 'nightly')).toBe('nightly');
  });

  it('submits one sanitized analytics event and marks delivery metadata', async () => {
    const dataRoot = await mkdtemp(path.join(tmpdir(), 'od-update-observe-'));
    const capture = vi.fn();
    try {
      const summaryPath = await writeSummary(dataRoot, pendingSummary());

      await expect(observePendingInstallerApplyAttempts({
        analytics: { capture },
        appVersion: '0.8.0-beta.6',
        currentChannel: 'beta',
        currentVersion: '0.8.0-beta.6',
        dataRoot,
        env: { POSTHOG_KEY: 'ph_test' },
        namespace: 'release-beta-win',
        now: () => new Date('2026-05-20T00:03:00.000Z'),
        readConfig: async () => ({
          installationId: 'install-1',
          telemetry: { metrics: true },
        }),
      })).resolves.toEqual({ observed: 1 });

      expect(capture).toHaveBeenCalledTimes(1);
      expect(capture.mock.calls[0]?.[0]).toMatchObject({
        eventName: 'update_apply_observed',
        appVersion: '0.8.0-beta.6',
        insertId: 'update_apply_observed:flow-1',
        properties: {
          arch: 'x64',
          artifact_type: 'installer',
          channel: 'beta',
          elapsed_bucket: 'lt_5m',
          flow_id: 'flow-1',
          from_version: '0.8.0-beta.5',
          namespace: 'release-beta-win',
          platform: 'win32',
          reason: 'app_version_matches',
          result: 'success',
          to_version: '0.8.0-beta.6',
        },
      });
      const updated = JSON.parse(await readFile(summaryPath, 'utf8')) as Record<string, unknown>;
      expect(updated).toMatchObject({
        delivery: {
          eventName: 'update_apply_observed',
          insertId: 'update_apply_observed:flow-1',
          status: 'submitted',
        },
        reason: 'app_version_matches',
        result: 'success',
      });

      await observePendingInstallerApplyAttempts({
        analytics: { capture },
        appVersion: '0.8.0-beta.6',
        currentVersion: '0.8.0-beta.6',
        dataRoot,
        env: { POSTHOG_KEY: 'ph_test' },
        namespace: 'release-beta-win',
      });
      expect(capture).toHaveBeenCalledTimes(1);
    } finally {
      await rm(dataRoot, { force: true, recursive: true });
    }
  });

  it('marks no-consent observations without submitting analytics', async () => {
    const dataRoot = await mkdtemp(path.join(tmpdir(), 'od-update-observe-'));
    const capture = vi.fn();
    try {
      const summaryPath = await writeSummary(dataRoot, pendingSummary());
      await observePendingInstallerApplyAttempts({
        analytics: { capture },
        appVersion: '0.8.0-beta.6',
        currentVersion: '0.8.0-beta.6',
        dataRoot,
        env: { POSTHOG_KEY: 'ph_test' },
        namespace: 'release-beta-win',
        readConfig: async () => ({
          installationId: null,
          telemetry: { metrics: false },
        }),
      });

      expect(capture).not.toHaveBeenCalled();
      const updated = JSON.parse(await readFile(summaryPath, 'utf8')) as Record<string, unknown>;
      expect(updated).toMatchObject({
        delivery: {
          eventName: 'update_apply_observed',
          status: 'skipped_no_consent',
        },
      });
    } finally {
      await rm(dataRoot, { force: true, recursive: true });
    }
  });
});
