import * as fs from 'fs/promises';
import * as nodeFs from 'fs';
import * as os from 'os';
import * as path from 'path';

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

const hoisted = vi.hoisted(() => ({
  teamsBase: '',
}));

vi.mock('../../../../src/main/utils/pathDecoder', () => ({
  getTeamsBasePath: () => hoisted.teamsBase,
}));

vi.mock('../../../../src/main/services/team/TeamFsWorkerClient', () => ({
  getTeamFsWorkerClient: () => ({
    isAvailable: () => false,
  }),
}));

import { TeamConfigReader } from '../../../../src/main/services/team/TeamConfigReader';
import { createPersistedLaunchSummaryProjection } from '../../../../src/main/services/team/TeamLaunchSummaryProjection';

function createDeferred<T>(): {
  promise: Promise<T>;
  resolve: (value: T) => void;
  reject: (error: unknown) => void;
} {
  let resolve!: (value: T) => void;
  let reject!: (error: unknown) => void;
  const promise = new Promise<T>((res, rej) => {
    resolve = res;
    reject = rej;
  });
  return { promise, resolve, reject };
}

describe('TeamConfigReader', () => {
  let tempDir = '';

  beforeEach(async () => {
    tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'team-config-reader-'));
    hoisted.teamsBase = tempDir;
  });

  afterEach(async () => {
    vi.useRealTimers();
    vi.restoreAllMocks();
    TeamConfigReader.clearCacheForTests();
    if (tempDir) {
      await fs.rm(tempDir, { recursive: true, force: true });
    }
    hoisted.teamsBase = '';
  });

  it('uses compact launch summary projection when launch-state.json is oversized', async () => {
    const teamName = 'mixed-team';
    const teamDir = path.join(tempDir, teamName);
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(
      path.join(teamDir, 'config.json'),
      JSON.stringify({
        name: 'Mixed Team',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );
    await fs.writeFile(path.join(teamDir, 'launch-state.json'), 'x'.repeat(40 * 1024), 'utf8');
    await fs.writeFile(
      path.join(teamDir, 'launch-summary.json'),
      JSON.stringify(
        createPersistedLaunchSummaryProjection({
          version: 2,
          teamName,
          updatedAt: '2026-04-22T12:00:00.000Z',
          launchPhase: 'finished',
          expectedMembers: ['alice', 'bob'],
          bootstrapExpectedMembers: ['alice'],
          members: {
            alice: {
              name: 'alice',
              providerId: 'codex',
              laneId: 'primary',
              laneKind: 'primary',
              laneOwnerProviderId: 'codex',
              launchState: 'confirmed_alive',
              agentToolAccepted: true,
              runtimeAlive: true,
              bootstrapConfirmed: true,
              hardFailure: false,
              lastEvaluatedAt: '2026-04-22T12:00:00.000Z',
            },
            bob: {
              name: 'bob',
              providerId: 'opencode',
              laneId: 'secondary:opencode:bob',
              laneKind: 'secondary',
              laneOwnerProviderId: 'opencode',
              launchState: 'failed_to_start',
              agentToolAccepted: true,
              runtimeAlive: false,
              bootstrapConfirmed: false,
              hardFailure: true,
              hardFailureReason: 'Side lane failed',
              lastEvaluatedAt: '2026-04-22T12:00:00.000Z',
            },
          },
          summary: {
            confirmedCount: 1,
            pendingCount: 0,
            failedCount: 1,
            runtimeAlivePendingCount: 0,
          },
          teamLaunchState: 'partial_failure',
        } as never),
        null,
        2
      ),
      'utf8'
    );
    await fs.writeFile(
      path.join(teamDir, 'bootstrap-state.json'),
      JSON.stringify({
        version: 1,
        teamName,
        runId: 'bootstrap-run-1',
        ownerPid: process.pid,
        startedAt: Date.parse('2026-04-22T12:01:00.000Z'),
        updatedAt: Date.parse('2026-04-22T12:01:00.000Z'),
        phase: 'spawning_members',
        members: [{ name: 'alice', status: 'pending' }],
      }),
      'utf8'
    );

    const reader = new TeamConfigReader();
    const teams = await reader.listTeams();
    expect(teams).toHaveLength(1);
    expect(teams[0]).toMatchObject({
      teamName,
      displayName: 'Mixed Team',
      partialLaunchFailure: true,
      expectedMemberCount: 2,
      confirmedMemberCount: 1,
      missingMembers: ['bob'],
      teamLaunchState: 'partial_failure',
      confirmedCount: 1,
      pendingCount: 0,
      failedCount: 1,
    });
  });

  it('does not invent a partial-failure summary from artifact counts for mixed-aware teams when canonical launch truth is unavailable', async () => {
    const teamName = 'mixed-aware-team';
    const teamDir = path.join(tempDir, teamName);
    await fs.mkdir(path.join(teamDir, 'inboxes'), { recursive: true });
    await fs.writeFile(
      path.join(teamDir, 'config.json'),
      JSON.stringify({
        name: 'Mixed Aware Team',
        leadSessionId: 'lead-session-1',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );
    await fs.writeFile(
      path.join(teamDir, 'team.meta.json'),
      JSON.stringify({
        version: 1,
        cwd: tempDir,
        providerId: 'codex',
        createdAt: Date.now(),
      }),
      'utf8'
    );
    await fs.writeFile(
      path.join(teamDir, 'members.meta.json'),
      JSON.stringify({
        version: 1,
        members: [
          { name: 'alice', providerId: 'codex', role: 'reviewer' },
          { name: 'tom', providerId: 'opencode', role: 'developer' },
        ],
      }),
      'utf8'
    );
    await fs.writeFile(path.join(teamDir, 'inboxes', 'alice.json'), '{}', 'utf8');

    const reader = new TeamConfigReader();
    const teams = await reader.listTeams();
    expect(teams).toHaveLength(1);
    expect(teams[0]).toMatchObject({
      teamName,
      displayName: 'Mixed Aware Team',
      memberCount: 2,
    });
    expect(teams[0]?.partialLaunchFailure).toBeUndefined();
    expect(teams[0]?.teamLaunchState).toBeUndefined();
    expect(teams[0]?.missingMembers).toBeUndefined();
  });

  it('exposes lead summary fields without adding lead to teammate member chips', async () => {
    const teamName = 'lead-summary-team';
    const teamDir = path.join(tempDir, teamName);
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(
      path.join(teamDir, 'config.json'),
      JSON.stringify({
        name: 'Lead Summary Team',
        members: [
          { name: 'captain', agentType: 'team-lead', color: '#123456' },
          { name: 'alice', role: 'reviewer', color: '#abcdef' },
        ],
      }),
      'utf8'
    );

    const reader = new TeamConfigReader();
    const teams = await reader.listTeams();

    expect(teams).toHaveLength(1);
    expect(teams[0]).toMatchObject({
      teamName,
      displayName: 'Lead Summary Team',
      memberCount: 1,
      members: [{ name: 'alice', role: 'reviewer', color: '#abcdef' }],
      leadName: 'captain',
      leadColor: '#123456',
    });
  });

  it('dedupes and briefly caches listTeams scans until invalidated', async () => {
    const teamName = 'cached-list-team';
    const teamDir = path.join(tempDir, teamName);
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(
      path.join(teamDir, 'config.json'),
      JSON.stringify({
        name: 'Cached List Team',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );
    const readdirSpy = vi.spyOn(nodeFs.promises, 'readdir');

    const reader = new TeamConfigReader();
    const [first, second] = await Promise.all([reader.listTeams(), reader.listTeams()]);
    const readdirAfterFirstBatch = readdirSpy.mock.calls.length;

    expect(first).toHaveLength(1);
    expect(second).toHaveLength(1);

    await reader.listTeams();
    expect(readdirSpy).toHaveBeenCalledTimes(readdirAfterFirstBatch);

    TeamConfigReader.invalidateTeam(teamName);
    await reader.listTeams();
    expect(readdirSpy.mock.calls.length).toBeGreaterThan(readdirAfterFirstBatch);
  });

  it('does not reuse a stale in-flight listTeams scan after invalidation', async () => {
    const teamName = 'inflight-invalidated-list-team';
    const teamDir = path.join(tempDir, teamName);
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(
      path.join(teamDir, 'config.json'),
      JSON.stringify({
        name: 'Before Invalidation',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );

    const firstReadStarted = createDeferred<void>();
    const releaseFirstRead = createDeferred<void>();
    const originalReaddir = nodeFs.promises.readdir.bind(nodeFs.promises);
    let blockedFirstTeamScan = false;
    const readdirSpy = vi
      .spyOn(nodeFs.promises, 'readdir')
      .mockImplementation(async (...args: unknown[]) => {
        if (!blockedFirstTeamScan && args[0] === tempDir) {
          blockedFirstTeamScan = true;
          firstReadStarted.resolve();
          await releaseFirstRead.promise;
        }
        return originalReaddir(...(args as Parameters<typeof nodeFs.promises.readdir>));
      });

    const reader = new TeamConfigReader();
    const first = reader.listTeams();
    await firstReadStarted.promise;

    await fs.writeFile(
      path.join(teamDir, 'config.json'),
      JSON.stringify({
        name: 'After Invalidation',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );
    TeamConfigReader.invalidateTeam(teamName);

    const second = reader.listTeams();
    await Promise.resolve();

    const teamDirReads = readdirSpy.mock.calls.filter((call) => call[0] === tempDir);
    expect(teamDirReads.length).toBeGreaterThanOrEqual(2);

    releaseFirstRead.resolve();
    const [, secondTeams] = await Promise.all([first, second]);
    expect(secondTeams[0]?.displayName).toBe('After Invalidation');
  });

  it('does not let a removed base member hide an active auto-suffixed teammate in team summaries', async () => {
    const teamName = 'suffix-team';
    const teamDir = path.join(tempDir, teamName);
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(
      path.join(teamDir, 'config.json'),
      JSON.stringify({
        name: 'Suffix Team',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );
    await fs.writeFile(
      path.join(teamDir, 'members.meta.json'),
      JSON.stringify({
        version: 1,
        members: [
          { name: 'alice', role: 'developer', removedAt: Date.now() - 60_000 },
          { name: 'alice-2', role: 'reviewer' },
        ],
      }),
      'utf8'
    );

    const reader = new TeamConfigReader();
    const teams = await reader.listTeams();
    expect(teams).toHaveLength(1);
    expect(teams[0]).toMatchObject({
      teamName,
      displayName: 'Suffix Team',
      memberCount: 1,
      members: [{ name: 'alice-2', role: 'reviewer' }],
    });
  });

  it('counts only active non-lead teammates for draft team summaries', async () => {
    const teamName = 'draft-summary-team';
    const teamDir = path.join(tempDir, teamName);
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(
      path.join(teamDir, 'team.meta.json'),
      JSON.stringify({
        version: 1,
        cwd: tempDir,
        displayName: 'Draft Summary Team',
        createdAt: Date.parse('2026-04-22T12:00:00.000Z'),
      }),
      'utf8'
    );
    await fs.writeFile(
      path.join(teamDir, 'members.meta.json'),
      JSON.stringify({
        version: 1,
        members: [
          { name: 'team-lead', agentType: 'team-lead', color: '#123456' },
          { name: 'alice', removedAt: Date.now() - 60_000 },
          { name: 'bob', role: 'developer' },
        ],
      }),
      'utf8'
    );

    const reader = new TeamConfigReader();
    const teams = await reader.listTeams();
    expect(teams).toHaveLength(1);
    expect(teams[0]).toMatchObject({
      teamName,
      displayName: 'Draft Summary Team',
      memberCount: 1,
      leadName: 'team-lead',
      leadColor: '#123456',
      pendingCreate: true,
    });
  });

  it('uses injected members meta store for draft team summaries', async () => {
    const teamName = 'draft-summary-injected-store-team';
    const teamDir = path.join(tempDir, teamName);
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(
      path.join(teamDir, 'team.meta.json'),
      JSON.stringify({
        version: 1,
        cwd: tempDir,
        displayName: 'Injected Draft Team',
        createdAt: Date.parse('2026-04-22T12:00:00.000Z'),
      }),
      'utf8'
    );
    await fs.writeFile(
      path.join(teamDir, 'members.meta.json'),
      JSON.stringify({ version: 1, members: [] }),
      'utf8'
    );
    const getMembers = vi.fn(async () => [
      { name: 'captain', agentType: 'team-lead', color: '#123456' },
      { name: 'alice', role: 'developer' },
    ]);

    const reader = new TeamConfigReader({ getMembers } as never);
    const teams = await reader.listTeams();

    expect(getMembers).toHaveBeenCalledWith(teamName);
    expect(teams[0]).toMatchObject({
      teamName,
      displayName: 'Injected Draft Team',
      memberCount: 1,
      leadName: 'captain',
      leadColor: '#123456',
      pendingCreate: true,
    });
  });

  it('shares in-flight verified reads without reusing completed cache', async () => {
    const teamName = 'cached-config-team';
    const teamDir = path.join(tempDir, teamName);
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(
      path.join(teamDir, 'config.json'),
      JSON.stringify({
        name: 'Cached Config Team',
        projectPath: tempDir,
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );
    const readFileSpy = vi.spyOn(nodeFs.promises, 'readFile');

    const reader = new TeamConfigReader();
    const [first, second] = await Promise.all([
      reader.getConfigVerified(teamName),
      reader.getConfigVerified(teamName),
    ]);
    if (!first) {
      throw new Error('Expected config to load.');
    }
    first.name = 'Mutated In Caller';
    const third = await reader.getConfigVerified(teamName);

    expect(second?.name).toBe('Cached Config Team');
    expect(third?.name).toBe('Cached Config Team');
    expect(readFileSpy).toHaveBeenCalledTimes(2);
  });

  it('uses fingerprint-validated snapshot cache without rereading unchanged config content', async () => {
    const teamName = 'snapshot-cache-team';
    const teamDir = path.join(tempDir, teamName);
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(
      path.join(teamDir, 'config.json'),
      JSON.stringify({
        name: 'Snapshot Cache Team',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );
    const readFileSpy = vi.spyOn(nodeFs.promises, 'readFile');
    const statSpy = vi.spyOn(nodeFs.promises, 'stat');

    const reader = new TeamConfigReader();
    const first = await reader.getConfigSnapshot(teamName);
    if (!first) {
      throw new Error('Expected config to load.');
    }
    first.name = 'Mutated In Caller';
    const second = await reader.getConfigSnapshot(teamName);

    expect(second?.name).toBe('Snapshot Cache Team');
    expect(statSpy).toHaveBeenCalledTimes(2);
    expect(readFileSpy).toHaveBeenCalledTimes(1);
  });

  it('logs slow config reads with mode, likely cause, generation, and caller diagnostics', async () => {
    const teamName = 'slow-read-diagnostics-team';
    const teamDir = path.join(tempDir, teamName);
    const configPath = path.join(teamDir, 'config.json');
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(
      configPath,
      JSON.stringify({
        name: 'Slow Diagnostics Team',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );
    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
    vi.spyOn(performance, 'now')
      .mockReturnValueOnce(0)
      .mockReturnValueOnce(0)
      .mockReturnValueOnce(1)
      .mockReturnValueOnce(1)
      .mockReturnValueOnce(2_001)
      .mockReturnValueOnce(2_001)
      .mockReturnValueOnce(2_001)
      .mockReturnValueOnce(2_001)
      .mockReturnValueOnce(2_001);

    const reader = new TeamConfigReader();
    expect((await reader.getConfigVerified(teamName))?.name).toBe('Slow Diagnostics Team');

    const slowLog = warnSpy.mock.calls.find((call) =>
      String(call[1] ?? '').includes('[getConfig] slow read diag=')
    );
    expect(slowLog).toBeTruthy();
    const rawMessage = String(slowLog?.[1] ?? '');
    const diag = JSON.parse(rawMessage.slice(rawMessage.indexOf('diag=') + 'diag='.length)) as {
      mode: string;
      configPath: string;
      likelyCause: string;
      readMs: number;
      cacheGeneration: number;
      currentGeneration: number;
      caller: string | null;
    };
    expect(diag).toMatchObject({
      mode: 'verified',
      configPath,
      likelyCause: 'io_read_slow',
      readMs: 2000,
      cacheGeneration: 0,
      currentGeneration: 0,
    });
    expect(diag.caller).toBeTruthy();
  });

  it('shares in-flight snapshot stat and read work for concurrent calls', async () => {
    const teamName = 'snapshot-inflight-team';
    const teamDir = path.join(tempDir, teamName);
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(
      path.join(teamDir, 'config.json'),
      JSON.stringify({
        name: 'Snapshot Inflight Team',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );
    const readFileSpy = vi.spyOn(nodeFs.promises, 'readFile');
    const statSpy = vi.spyOn(nodeFs.promises, 'stat');

    const reader = new TeamConfigReader();
    const [first, second] = await Promise.all([
      reader.getConfigSnapshot(teamName),
      reader.getConfigSnapshot(teamName),
    ]);

    expect(first?.name).toBe('Snapshot Inflight Team');
    expect(second?.name).toBe('Snapshot Inflight Team');
    expect(statSpy).toHaveBeenCalledTimes(1);
    expect(readFileSpy).toHaveBeenCalledTimes(1);
  });

  it('rereads snapshot when ctime changes even if mtime is unchanged', async () => {
    const teamName = 'snapshot-ctime-team';
    const teamDir = path.join(tempDir, teamName);
    const configPath = path.join(teamDir, 'config.json');
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(
      configPath,
      JSON.stringify({
        name: 'Before Ctime',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );
    let ctimeMs = 1000;
    vi.spyOn(nodeFs.promises, 'stat').mockImplementation(async () => ({
      size: BigInt(4096),
      mode: BigInt(33188),
      dev: BigInt(1),
      ino: BigInt(2),
      mtimeMs: 1000,
      ctimeMs,
      birthtimeMs: 1000,
      isFile: () => true,
    }) as never);
    const readFileSpy = vi.spyOn(nodeFs.promises, 'readFile');

    const reader = new TeamConfigReader();
    expect((await reader.getConfigSnapshot(teamName))?.name).toBe('Before Ctime');
    await fs.writeFile(
      configPath,
      JSON.stringify({
        name: 'After Ctime',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );
    ctimeMs = 2000;

    expect((await reader.getConfigSnapshot(teamName))?.name).toBe('After Ctime');
    expect(readFileSpy).toHaveBeenCalledTimes(2);
  });

  it('rereads snapshot when the config fingerprint changes', async () => {
    const teamName = 'snapshot-reread-team';
    const teamDir = path.join(tempDir, teamName);
    const configPath = path.join(teamDir, 'config.json');
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(
      configPath,
      JSON.stringify({
        name: 'Before',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );
    const readFileSpy = vi.spyOn(nodeFs.promises, 'readFile');

    const reader = new TeamConfigReader();
    expect((await reader.getConfigSnapshot(teamName))?.name).toBe('Before');
    await fs.writeFile(
      configPath,
      JSON.stringify({
        name: 'After',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );

    expect((await reader.getConfigSnapshot(teamName))?.name).toBe('After');
    expect(readFileSpy).toHaveBeenCalledTimes(2);
  });

  it('primeConfig updates snapshot cache immediately after app-owned writes', async () => {
    const teamName = 'prime-cache-team';
    const teamDir = path.join(tempDir, teamName);
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(
      path.join(teamDir, 'config.json'),
      JSON.stringify({
        name: 'Before Prime',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );

    const reader = new TeamConfigReader();
    expect((await reader.getConfigSnapshot(teamName))?.name).toBe('Before Prime');
    await fs.writeFile(
      path.join(teamDir, 'config.json'),
      JSON.stringify({
        name: 'After Prime',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );
    await TeamConfigReader.primeConfig(teamName, {
      name: 'After Prime',
      members: [{ name: 'team-lead', agentType: 'team-lead' }],
    } as never);

    const snapshot = await reader.getConfigSnapshot(teamName);
    expect(snapshot?.name).toBe('After Prime');
  });

  it('does not let stale in-flight snapshot reads overwrite a primed config cache', async () => {
    const teamName = 'stale-read-prime-team';
    const teamDir = path.join(tempDir, teamName);
    const configPath = path.join(teamDir, 'config.json');
    await fs.mkdir(teamDir, { recursive: true });
    const staleRaw = JSON.stringify({
      name: 'Stale Read',
      members: [{ name: 'team-lead', agentType: 'team-lead' }],
    });
    await fs.writeFile(configPath, staleRaw, 'utf8');

    const readDeferred = createDeferred<string>();
    const realReadFile = nodeFs.promises.readFile.bind(nodeFs.promises);
    let intercepted = false;
    vi.spyOn(nodeFs.promises, 'readFile').mockImplementation(
      ((file: unknown, ...args: unknown[]) => {
        if (!intercepted && String(file) === configPath) {
          intercepted = true;
          return readDeferred.promise as never;
        }
        return realReadFile(file as never, ...(args as never[])) as never;
      }) as never
    );

    const reader = new TeamConfigReader();
    const staleSnapshot = reader.getConfigSnapshot(teamName);
    await vi.waitFor(() => expect(intercepted).toBe(true));

    await fs.writeFile(
      configPath,
      JSON.stringify({
        name: 'Fresh Prime',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );
    await TeamConfigReader.primeConfig(teamName, {
      name: 'Fresh Prime',
      members: [{ name: 'team-lead', agentType: 'team-lead' }],
    } as never);

    readDeferred.resolve(staleRaw);
    expect((await staleSnapshot)?.name).toBe('Stale Read');

    vi.spyOn(nodeFs.promises, 'stat').mockRejectedValue(new Error('stat unavailable'));
    expect((await reader.getConfigSnapshot(teamName))?.name).toBe('Fresh Prime');
  });

  it('does not reuse stale in-flight verified reads after app-owned primeConfig', async () => {
    const teamName = 'verified-stale-read-prime-team';
    const teamDir = path.join(tempDir, teamName);
    const configPath = path.join(teamDir, 'config.json');
    await fs.mkdir(teamDir, { recursive: true });
    const staleRaw = JSON.stringify({
      name: 'Stale Verified Read',
      members: [{ name: 'team-lead', agentType: 'team-lead' }],
    });
    await fs.writeFile(configPath, staleRaw, 'utf8');

    const readDeferred = createDeferred<string>();
    const realReadFile = nodeFs.promises.readFile.bind(nodeFs.promises);
    let intercepted = false;
    vi.spyOn(nodeFs.promises, 'readFile').mockImplementation(
      ((file: unknown, ...args: unknown[]) => {
        if (!intercepted && String(file) === configPath) {
          intercepted = true;
          return readDeferred.promise as never;
        }
        return realReadFile(file as never, ...(args as never[])) as never;
      }) as never
    );

    const reader = new TeamConfigReader();
    const staleVerified = reader.getConfig(teamName);
    await vi.waitFor(() => expect(intercepted).toBe(true));

    await fs.writeFile(
      configPath,
      JSON.stringify({
        name: 'Fresh Verified Prime',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );
    await TeamConfigReader.primeConfig(teamName, {
      name: 'Fresh Verified Prime',
      members: [{ name: 'team-lead', agentType: 'team-lead' }],
    } as never);

    expect((await reader.getConfig(teamName))?.name).toBe('Fresh Verified Prime');

    readDeferred.resolve(staleRaw);
    expect((await staleVerified)?.name).toBe('Stale Verified Read');
    expect((await reader.getConfigSnapshot(teamName))?.name).toBe('Fresh Verified Prime');
  });

  it('does not let stale in-flight snapshot read failures invalidate a primed config cache', async () => {
    const teamName = 'stale-read-failure-prime-team';
    const teamDir = path.join(tempDir, teamName);
    const configPath = path.join(teamDir, 'config.json');
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(
      configPath,
      JSON.stringify({
        name: 'Before Failure',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );

    const readDeferred = createDeferred<string>();
    const realReadFile = nodeFs.promises.readFile.bind(nodeFs.promises);
    let intercepted = false;
    vi.spyOn(nodeFs.promises, 'readFile').mockImplementation(
      ((file: unknown, ...args: unknown[]) => {
        if (!intercepted && String(file) === configPath) {
          intercepted = true;
          return readDeferred.promise as never;
        }
        return realReadFile(file as never, ...(args as never[])) as never;
      }) as never
    );

    const reader = new TeamConfigReader();
    const staleSnapshot = reader.getConfigSnapshot(teamName);
    await vi.waitFor(() => expect(intercepted).toBe(true));

    await fs.writeFile(
      configPath,
      JSON.stringify({
        name: 'Fresh After Failure',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );
    await TeamConfigReader.primeConfig(teamName, {
      name: 'Fresh After Failure',
      members: [{ name: 'team-lead', agentType: 'team-lead' }],
    } as never);

    readDeferred.reject(new Error('old read failed'));
    await expect(staleSnapshot).resolves.toBeNull();

    vi.spyOn(nodeFs.promises, 'stat').mockRejectedValue(new Error('stat unavailable'));
    expect((await reader.getConfigSnapshot(teamName))?.name).toBe('Fresh After Failure');
  });

  it('does not let stale in-flight snapshot stat results invalidate a primed config cache', async () => {
    const teamName = 'stale-stat-prime-team';
    const teamDir = path.join(tempDir, teamName);
    const configPath = path.join(teamDir, 'config.json');
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(
      configPath,
      JSON.stringify({
        name: 'Before Stat Race',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );

    const statDeferred = createDeferred<unknown>();
    let statCalls = 0;
    vi.spyOn(nodeFs.promises, 'stat').mockImplementation(async () => {
      statCalls++;
      if (statCalls === 1) {
        return (await statDeferred.promise) as never;
      }
      throw new Error('stat unavailable');
    });

    const reader = new TeamConfigReader();
    const snapshot = reader.getConfigSnapshot(teamName);
    await vi.waitFor(() => expect(statCalls).toBe(1));

    await fs.writeFile(
      configPath,
      JSON.stringify({
        name: 'Fresh After Stat Race',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );
    await TeamConfigReader.primeConfig(teamName, {
      name: 'Fresh After Stat Race',
      members: [{ name: 'team-lead', agentType: 'team-lead' }],
    } as never);

    statDeferred.resolve({
      size: BigInt(4096),
      mode: BigInt(33188),
      dev: BigInt(1),
      ino: BigInt(2),
      mtimeMs: 1000,
      ctimeMs: 1000,
      birthtimeMs: 1000,
      isFile: () => false,
    });

    expect((await snapshot)?.name).toBe('Fresh After Stat Race');
  });

  it('invalidateTeam forces the next snapshot to reread config content', async () => {
    const teamName = 'invalidate-cache-team';
    const teamDir = path.join(tempDir, teamName);
    const configPath = path.join(teamDir, 'config.json');
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(
      configPath,
      JSON.stringify({
        name: 'Before Invalidate',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );
    const readFileSpy = vi.spyOn(nodeFs.promises, 'readFile');

    const reader = new TeamConfigReader();
    expect((await reader.getConfigSnapshot(teamName))?.name).toBe('Before Invalidate');
    await fs.writeFile(
      configPath,
      JSON.stringify({
        name: 'After Invalidate',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );
    TeamConfigReader.invalidateTeam(teamName);

    expect((await reader.getConfigSnapshot(teamName))?.name).toBe('After Invalidate');
    expect(readFileSpy).toHaveBeenCalledTimes(2);
  });

  it('uses recent snapshot cache on stat failure but verified mode does not', async () => {
    const teamName = 'stat-failure-team';
    const teamDir = path.join(tempDir, teamName);
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(
      path.join(teamDir, 'config.json'),
      JSON.stringify({
        name: 'Recent Cache',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );

    const reader = new TeamConfigReader();
    expect((await reader.getConfigSnapshot(teamName))?.name).toBe('Recent Cache');
    vi.spyOn(nodeFs.promises, 'stat').mockRejectedValue(new Error('stat unavailable'));

    expect((await reader.getConfigSnapshot(teamName))?.name).toBe('Recent Cache');
    await expect(reader.getConfigVerified(teamName)).resolves.toBeNull();
  });

  it('clears snapshot cache after parse failure', async () => {
    const teamName = 'parse-failure-team';
    const teamDir = path.join(tempDir, teamName);
    const configPath = path.join(teamDir, 'config.json');
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(
      configPath,
      JSON.stringify({
        name: 'Valid Config',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );

    const reader = new TeamConfigReader();
    expect((await reader.getConfigSnapshot(teamName))?.name).toBe('Valid Config');
    await fs.writeFile(configPath, '{"name":', 'utf8');

    expect(await reader.getConfigSnapshot(teamName)).toBeNull();
    await fs.rm(configPath);
    expect(await reader.getConfigSnapshot(teamName)).toBeNull();
  });

  it('clears snapshot cache when config disappears and reloads after recreation', async () => {
    const teamName = 'missing-then-recreated-team';
    const teamDir = path.join(tempDir, teamName);
    const configPath = path.join(teamDir, 'config.json');
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(
      configPath,
      JSON.stringify({
        name: 'Before Delete',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );

    const reader = new TeamConfigReader();
    expect((await reader.getConfigSnapshot(teamName))?.name).toBe('Before Delete');
    await fs.rm(configPath);

    expect(await reader.getConfigSnapshot(teamName)).toBeNull();
    await fs.writeFile(
      configPath,
      JSON.stringify({
        name: 'After Recreate',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );

    expect((await reader.getConfigSnapshot(teamName))?.name).toBe('After Recreate');
  });

  it('bounds stale snapshots on coarse fingerprints with periodic full verification', async () => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date('2026-04-30T12:00:00.000Z'));

    const teamName = 'coarse-fs-team';
    const teamDir = path.join(tempDir, teamName);
    const configPath = path.join(teamDir, 'config.json');
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(
      configPath,
      JSON.stringify({
        name: 'Alpha',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );
    const readFileSpy = vi.spyOn(nodeFs.promises, 'readFile');
    vi.spyOn(nodeFs.promises, 'stat').mockResolvedValue({
      size: BigInt(4096),
      mode: BigInt(33188),
      dev: BigInt(1),
      ino: BigInt(2),
      mtimeMs: 1000,
      ctimeMs: 1000,
      birthtimeMs: 1000,
      isFile: () => true,
    } as never);

    const reader = new TeamConfigReader();
    expect((await reader.getConfigSnapshot(teamName))?.name).toBe('Alpha');
    await fs.writeFile(
      configPath,
      JSON.stringify({
        name: 'Bravo',
        members: [{ name: 'team-lead', agentType: 'team-lead' }],
      }),
      'utf8'
    );

    expect((await reader.getConfigSnapshot(teamName))?.name).toBe('Alpha');
    vi.advanceTimersByTime(1_501);
    expect((await reader.getConfigSnapshot(teamName))?.name).toBe('Bravo');
    expect(readFileSpy).toHaveBeenCalledTimes(2);
  });
});
