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

const emit = vi.hoisted(() => vi.fn());

// Only `createLogger` is mocked: the module under test imports nothing else
// from the logger. The diagnostic is opt-in via `debugEnvVar` and emits at
// `debug`, so the assertions drive that child logger.
vi.mock('../../../src/core/logger.js', () => ({
  createLogger: () => ({
    warn: vi.fn(),
    info: vi.fn(),
    error: vi.fn(),
    debug: emit,
    trace: vi.fn(),
    fatal: vi.fn(),
    isLevelEnabled: () => true,
  }),
}));

import { logUnresolvedReceiverFiles } from '../../../src/core/ingestion/scope-resolution/unresolved-receivers.js';
import type { ResolutionOutcome } from '../../../src/core/ingestion/scope-resolution/resolution-outcome.js';

/**
 * #2837 / #2843 review. This diagnostic exists so a reporter can see WHICH file
 * lost its receivers. It must therefore count the same drops
 * `summarizeUnresolvedReceivers` counts — the first version omitted both of that
 * function's guards and its top-offender list was led by files that lost
 * nothing (measured on this repo: the top three were all test files).
 */
describe('logUnresolvedReceiverFiles (#2843 review)', () => {
  beforeEach(() => emit.mockClear());

  const drop = (filePath: string, over: Partial<ResolutionOutcome> = {}): ResolutionOutcome =>
    ({
      kind: 'suppressed',
      phase: 'receiver-bound-calls',
      reason: 'receiver-unresolved',
      filePath,
      name: 'DoWork',
      range: { startLine: 1, startCol: 0, endLine: 1, endCol: 1 },
      candidateIds: [],
      ...over,
    }) as ResolutionOutcome;

  const payload = (): {
    totalSites: number;
    filesAffected: number;
    topFiles: { filePath: string; sites: number }[];
  } => emit.mock.calls[0]![0] as never;

  it('says nothing when there are no receiver-unresolved drops', () => {
    logUnresolvedReceiverFiles([]);
    expect(emit).not.toHaveBeenCalled();
  });

  it('counts in-program call drops per file', () => {
    logUnresolvedReceiverFiles([drop('a.go'), drop('a.go'), drop('b.go')]);
    expect(payload().topFiles).toEqual([
      { filePath: 'a.go', sites: 2 },
      { filePath: 'b.go', sites: 1 },
    ]);
    expect(payload().totalSites).toBe(3);
  });

  // Guard 1, mirroring unresolved-receivers.ts: property reads/writes are not
  // call sites. Measured there at 25 of 124 drops on the fixture corpus.
  it('excludes non-call sites', () => {
    logUnresolvedReceiverFiles([
      drop('a.go'),
      drop('a.go', { siteKind: 'read' }),
      drop('a.go', { siteKind: 'write' }),
    ]);
    expect(payload().topFiles).toEqual([{ filePath: 'a.go', sites: 1 }]);
  });

  // Guard 2: an external-rooted receiver (`fmt.Println`) reaches code this index
  // does not contain, so no edge was ever possible and nothing was lost.
  it('excludes external-rooted drops', () => {
    logUnresolvedReceiverFiles([drop('a.go'), drop('a.go', { receiverOrigin: 'external' })]);
    expect(payload().topFiles).toEqual([{ filePath: 'a.go', sites: 1 }]);
  });

  // `unknown` origin counts WITH in-program: assuming a completeness we cannot
  // demonstrate is the unsafe direction.
  it('counts unknown-origin drops', () => {
    logUnresolvedReceiverFiles([drop('a.go', { receiverOrigin: 'unknown' })]);
    expect(payload().topFiles).toEqual([{ filePath: 'a.go', sites: 1 }]);
  });

  it('ranks by count then by code units, and caps the sample at ten files', () => {
    const outcomes = [
      ...Array.from({ length: 12 }, (_, i) => drop(`f${String(i).padStart(2, '0')}.go`)),
      drop('zz.go'),
      drop('zz.go'),
    ];
    logUnresolvedReceiverFiles(outcomes);
    expect(payload().topFiles).toHaveLength(10);
    expect(payload().topFiles[0]).toEqual({ filePath: 'zz.go', sites: 2 });
    // Ties break by code-unit order, so the survivors of the cap are stable.
    expect(
      payload()
        .topFiles.slice(1)
        .map((f) => f.filePath),
    ).toEqual([
      'f00.go',
      'f01.go',
      'f02.go',
      'f03.go',
      'f04.go',
      'f05.go',
      'f06.go',
      'f07.go',
      'f08.go',
    ]);
    // The total is the true count, not the capped sample.
    expect(payload().totalSites).toBe(14);
    expect(payload().filesAffected).toBe(13);
  });
});
