/*
 * Priority Forge — Concurrency regression tests.
 *
 * Structured in describe() blocks per fix (CONC-001 through CONC-007). Each
 * block is added progressively as the corresponding task lands. Run via
 *   npm test -- tests/concurrency.test.ts
 * Or to pin to a single block:
 *   npm test -- tests/concurrency.test.ts -t "CONC-001"
 *
 * Negative-control verification (per plan's Verification section):
 *   git stash                            # undo concurrency fixes
 *   npm test -- tests/concurrency.test   # expect: failures proving tests catch races
 *   git stash pop
 *   npm test -- tests/concurrency.test   # expect: all pass
 */

import { describe, it, expect, afterEach } from 'vitest';
import { Mutex } from 'async-mutex';
import { JsonStorage } from '../src/storage/jsonStorage';
import {
  TaskAlreadyExistsError,
  VersionConflictError,
  TaskAlreadyClaimedError,
  TaskNotClaimableError,
} from '../src/storage/errors';
import {
  withIdempotencyKey,
  _clearIdempotencyForTests,
  _idempotencyCacheSizes,
} from '../src/utils/idempotency';

// Test data that needs cleanup after each test. Tasks created during tests
// should be registered here so afterEach can delete them, preventing pollution
// of the real progress.json.
const tasksToCleanup: Array<{ storage: JsonStorage; id: string }> = [];

// Track instances created in tests so we can tear down their file watchers.
// The chokidar watcher would otherwise keep the process alive after tests finish.
const instances: JsonStorage[] = [];

afterEach(async () => {
  // Clean up test-created tasks before tearing down their storage instance
  for (const { storage, id } of tasksToCleanup.splice(0)) {
    try {
      await storage.deleteTask(id);
    } catch {
      /* task may not exist (e.g. test already deleted it) — fine */
    }
  }
  for (const s of instances.splice(0)) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (s as any).stopFileWatcher?.();
  }
});

function createStorage(): JsonStorage {
  const s = new JsonStorage();
  instances.push(s);
  return s;
}

describe('CONC-001 — Write mutex foundation', () => {
  // Sanity check the library itself — if this fails, async-mutex is broken
  // or not installed. Independent of our JsonStorage integration.
  it('async-mutex serializes concurrent runExclusive calls', async () => {
    const mutex = new Mutex();
    let inFlight = 0;
    let maxInFlight = 0;

    const op = () =>
      mutex.runExclusive(async () => {
        inFlight++;
        if (inFlight > maxInFlight) maxInFlight = inFlight;
        // Force a real async yield so the scheduler has opportunity to
        // interleave other tasks between increment and decrement.
        await new Promise((r) => setImmediate(r));
        inFlight--;
      });

    await Promise.all(Array.from({ length: 50 }, op));
    expect(maxInFlight).toBe(1);
  });

  it('save() serializes concurrent calls (workspace mutex)', async () => {
    const s = createStorage();
    // Reset instrumentation in case the singleton's own startup incremented it
    s._maxWriteInFlight.workspace = 0;
    s._writeInFlight.workspace = 0;

    // Fire 50 concurrent save()s. Without the mutex the counter would observe
    // multiple concurrent writers (because await points inside _saveLocked
    // yield to other promises). With the mutex, exactly one runs at a time.
    await Promise.all(
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      Array.from({ length: 50 }, () => (s as any).save()),
    );

    expect(s._maxWriteInFlight.workspace).toBe(1);
    expect(s._writeInFlight.workspace).toBe(0); // all drained
  });

  it('saveGlobalML() serializes concurrent calls (globalML mutex)', async () => {
    const s = createStorage();
    s._maxWriteInFlight.globalML = 0;
    s._writeInFlight.globalML = 0;

    await Promise.all(
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      Array.from({ length: 50 }, () => (s as any).saveGlobalML()),
    );

    expect(s._maxWriteInFlight.globalML).toBe(1);
    expect(s._writeInFlight.globalML).toBe(0);
  });

  it('workspace + globalML mutexes are independent (no cross-locking)', async () => {
    const s = createStorage();
    s._maxWriteInFlight.workspace = 0;
    s._maxWriteInFlight.globalML = 0;
    s._writeInFlight.workspace = 0;
    s._writeInFlight.globalML = 0;

    // Interleave: 25 each, launched concurrently. workspace writes shouldn't
    // contend with globalML writes (separate mutexes = parallelism preserved
    // across files).
    await Promise.all([
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      ...Array.from({ length: 25 }, () => (s as any).save()),
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      ...Array.from({ length: 25 }, () => (s as any).saveGlobalML()),
    ]);

    // Each mutex serializes its own file, so max per-file is 1
    expect(s._maxWriteInFlight.workspace).toBe(1);
    expect(s._maxWriteInFlight.globalML).toBe(1);
    expect(s._writeInFlight.workspace).toBe(0);
    expect(s._writeInFlight.globalML).toBe(0);
  });

  it('runExclusiveWorkspace/GlobalML expose the mutexes for broader critical sections', async () => {
    const s = createStorage();

    // The helpers from CONC-001 let later tasks (CONC-002 createTask race fix,
    // CONC-006 dual-write) wrap check-then-write blocks atomically.
    let ran = 0;
    await s.runExclusiveWorkspace(async () => {
      ran++;
    });
    await s.runExclusiveGlobalML(async () => {
      ran++;
    });
    expect(ran).toBe(2);
  });
});

describe('CONC-002 — createTask TOCTOU fix', () => {
  it('two concurrent creates with the same custom id — exactly one succeeds, one rejects with TaskAlreadyExistsError', async () => {
    const s = createStorage();
    const id = `test-toctou-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    tasksToCleanup.push({ storage: s, id });

    // Fire both creates at once. Before CONC-002, both would pass the
    // `taskMap.has(id)` check (pre-mutex) and both would land in taskMap —
    // last-write-wins, one silently overwritten. After the fix, the check
    // happens inside the workspaceMutex so one call throws.
    const results = await Promise.allSettled([
      s.createTask({
        id,
        task: 'concurrent A',
        priority: 'P2',
        project: 'test-concurrency',
        effort: 'low',
      }),
      s.createTask({
        id,
        task: 'concurrent B',
        priority: 'P2',
        project: 'test-concurrency',
        effort: 'low',
      }),
    ]);

    const fulfilled = results.filter((r) => r.status === 'fulfilled');
    const rejected = results.filter((r) => r.status === 'rejected');

    expect(fulfilled.length).toBe(1);
    expect(rejected.length).toBe(1);
    expect((rejected[0] as PromiseRejectedResult).reason).toBeInstanceOf(
      TaskAlreadyExistsError,
    );
    expect((rejected[0] as PromiseRejectedResult).reason.code).toBe(
      'TASK_ALREADY_EXISTS',
    );
    expect((rejected[0] as PromiseRejectedResult).reason.status).toBe(409);
  });

  it('50-way concurrent burst with same id — exactly one winner', async () => {
    const s = createStorage();
    const id = `test-toctou-burst-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    tasksToCleanup.push({ storage: s, id });

    const results = await Promise.allSettled(
      Array.from({ length: 50 }, (_, i) =>
        s.createTask({
          id,
          task: `burst ${i}`,
          priority: 'P2',
          project: 'test-concurrency',
          effort: 'low',
        }),
      ),
    );

    const fulfilled = results.filter((r) => r.status === 'fulfilled');
    const rejected = results.filter((r) => r.status === 'rejected');

    expect(fulfilled.length).toBe(1);
    expect(rejected.length).toBe(49);
    for (const r of rejected) {
      expect((r as PromiseRejectedResult).reason).toBeInstanceOf(
        TaskAlreadyExistsError,
      );
    }
  });

  it('different ids are not blocked by each other (no over-serialization)', async () => {
    const s = createStorage();
    const ids = Array.from(
      { length: 10 },
      (_, i) =>
        `test-distinct-${Date.now()}-${i}-${Math.random().toString(36).slice(2, 8)}`,
    );
    for (const id of ids) tasksToCleanup.push({ storage: s, id });

    // All 10 should succeed — the mutex serializes them internally but none
    // have id collisions so all land.
    const results = await Promise.allSettled(
      ids.map((id) =>
        s.createTask({
          id,
          task: `distinct ${id}`,
          priority: 'P2',
          project: 'test-concurrency',
          effort: 'low',
        }),
      ),
    );

    expect(results.every((r) => r.status === 'fulfilled')).toBe(true);
  });
});

describe('CONC-003 — optimistic concurrency on updateTask', () => {
  it('new tasks start at version 1', async () => {
    const s = createStorage();
    const id = `test-ver-new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    tasksToCleanup.push({ storage: s, id });

    const task = await s.createTask({
      id,
      task: 'versioned',
      priority: 'P2',
      project: 'test-concurrency',
      effort: 'low',
    });
    expect(task.version).toBe(1);
  });

  it('update bumps version by 1', async () => {
    const s = createStorage();
    const id = `test-ver-bump-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    tasksToCleanup.push({ storage: s, id });

    await s.createTask({
      id,
      task: 'v1',
      priority: 'P2',
      project: 'test-concurrency',
      effort: 'low',
    });
    const v2 = await s.updateTask(id, { notes: 'after update 1' });
    expect(v2?.version).toBe(2);
    const v3 = await s.updateTask(id, { notes: 'after update 2' });
    expect(v3?.version).toBe(3);
  });

  it('update without expectedVersion is unconditional (backwards-compat)', async () => {
    const s = createStorage();
    const id = `test-ver-uncond-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    tasksToCleanup.push({ storage: s, id });

    await s.createTask({
      id,
      task: 'orig',
      priority: 'P2',
      project: 'test-concurrency',
      effort: 'low',
    });
    // Omit expectedVersion: succeeds regardless of current version
    const updated = await s.updateTask(id, { notes: 'unconditional' });
    expect(updated?.notes).toBe('unconditional');
    expect(updated?.version).toBe(2);
  });

  it('update with matching expectedVersion succeeds', async () => {
    const s = createStorage();
    const id = `test-ver-match-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    tasksToCleanup.push({ storage: s, id });

    const created = await s.createTask({
      id,
      task: 'orig',
      priority: 'P2',
      project: 'test-concurrency',
      effort: 'low',
    });
    expect(created.version).toBe(1);
    const updated = await s.updateTask(id, { notes: 'with CAS' }, 1);
    expect(updated?.version).toBe(2);
  });

  it('update with stale expectedVersion rejects with VersionConflictError', async () => {
    const s = createStorage();
    const id = `test-ver-stale-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    tasksToCleanup.push({ storage: s, id });

    await s.createTask({
      id,
      task: 'orig',
      priority: 'P2',
      project: 'test-concurrency',
      effort: 'low',
    });
    // First update goes through, bumps to v2
    await s.updateTask(id, { notes: 'first' });
    // Second update claims expectedVersion=1 (stale) — should reject
    await expect(
      s.updateTask(id, { notes: 'stale writer' }, 1),
    ).rejects.toBeInstanceOf(VersionConflictError);
    // Verify the error carries diagnostic fields
    try {
      await s.updateTask(id, { notes: 'stale writer retry' }, 1);
    } catch (e) {
      expect(e).toBeInstanceOf(VersionConflictError);
      const err = e as VersionConflictError;
      expect(err.taskId).toBe(id);
      expect(err.expectedVersion).toBe(1);
      expect(err.currentVersion).toBe(2);
      expect(err.status).toBe(412);
      expect(err.code).toBe('VERSION_CONFLICT');
    }
  });

  it('two concurrent updates with same expectedVersion — one succeeds, one conflicts', async () => {
    const s = createStorage();
    const id = `test-ver-race-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    tasksToCleanup.push({ storage: s, id });

    const created = await s.createTask({
      id,
      task: 'orig',
      priority: 'P2',
      project: 'test-concurrency',
      effort: 'low',
    });
    expect(created.version).toBe(1);

    // Both agents read version 1 and attempt to update with expectedVersion=1.
    // The mutex serializes; one lands (bumps to 2), the other finds version=2
    // and throws VersionConflictError.
    const results = await Promise.allSettled([
      s.updateTask(id, { notes: 'agent A' }, 1),
      s.updateTask(id, { notes: 'agent B' }, 1),
    ]);

    const fulfilled = results.filter((r) => r.status === 'fulfilled');
    const rejected = results.filter((r) => r.status === 'rejected');
    expect(fulfilled.length).toBe(1);
    expect(rejected.length).toBe(1);
    expect((rejected[0] as PromiseRejectedResult).reason).toBeInstanceOf(
      VersionConflictError,
    );

    // Final state reflects the winner's update (version 2)
    const final = await s.getTask(id);
    expect(final?.version).toBe(2);
    expect(['agent A', 'agent B']).toContain(final?.notes);
  });

  it('existing pre-CONC-003 tasks treat as version 1', async () => {
    const s = createStorage();
    const id = `test-ver-legacy-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    tasksToCleanup.push({ storage: s, id });

    await s.createTask({
      id,
      task: 'legacy sim',
      priority: 'P2',
      project: 'test-concurrency',
      effort: 'low',
    });
    // Simulate a pre-version task by erasing the version field directly
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const t = (s as any).taskMap.get(id);
    delete t.version;
    // Now updating with expectedVersion=1 should succeed (legacy → treat as 1)
    const updated = await s.updateTask(id, { notes: 'legacy update' }, 1);
    expect(updated?.version).toBe(2); // bumped from "virtual 1" to 2
    // And expectedVersion=2 should reject (current is now 2, not 1)
    await expect(
      s.updateTask(id, { notes: 'retry' }, 1),
    ).rejects.toBeInstanceOf(VersionConflictError);
  });
});

describe('CONC-007 — Idempotency-Key on POST /tasks', () => {
  // These tests exercise the idempotency utility directly, which is what
  // the HTTP layer calls into. Full end-to-end HTTP tests can be added in
  // CONC-008 if desired.

  it('two calls with the same key return the same task (factory invoked once)', async () => {
    const s = createStorage();
    _clearIdempotencyForTests();

    const key = `test-idem-same-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    let factoryCalls = 0;
    const factory = async () => {
      factoryCalls++;
      const t = await s.createTask({
        task: 'idempotent create',
        priority: 'P2',
        project: 'test-concurrency',
        effort: 'low',
      });
      tasksToCleanup.push({ storage: s, id: t.id });
      return t;
    };

    const t1 = await withIdempotencyKey(key, factory, (id) => s.getTask(id));
    const t2 = await withIdempotencyKey(key, factory, (id) => s.getTask(id));

    expect(t1.id).toBe(t2.id);
    expect(factoryCalls).toBe(1);
  });

  it('concurrent calls with the same key share one promise (factory called once)', async () => {
    const s = createStorage();
    _clearIdempotencyForTests();

    const key = `test-idem-concurrent-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    let factoryCalls = 0;
    const factory = async () => {
      factoryCalls++;
      await new Promise((r) => setImmediate(r)); // force overlap
      const t = await s.createTask({
        task: 'concurrent idempotent',
        priority: 'P2',
        project: 'test-concurrency',
        effort: 'low',
      });
      tasksToCleanup.push({ storage: s, id: t.id });
      return t;
    };

    const results = await Promise.all(
      Array.from({ length: 10 }, () =>
        withIdempotencyKey(key, factory, (id) => s.getTask(id)),
      ),
    );

    const uniqueIds = new Set(results.map((r) => r.id));
    expect(uniqueIds.size).toBe(1);
    expect(factoryCalls).toBe(1);
  });

  it('different keys are not deduplicated (both factories invoked)', async () => {
    const s = createStorage();
    _clearIdempotencyForTests();

    const k1 = `test-idem-k1-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    const k2 = `test-idem-k2-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    let factoryCalls = 0;
    const make = (label: string) => async () => {
      factoryCalls++;
      const t = await s.createTask({
        task: label,
        priority: 'P2',
        project: 'test-concurrency',
        effort: 'low',
      });
      tasksToCleanup.push({ storage: s, id: t.id });
      return t;
    };

    const t1 = await withIdempotencyKey(k1, make('for-k1'), (id) => s.getTask(id));
    const t2 = await withIdempotencyKey(k2, make('for-k2'), (id) => s.getTask(id));
    expect(t1.id).not.toBe(t2.id);
    expect(factoryCalls).toBe(2);
  });

  it('stale cache (task deleted) → factory re-invoked', async () => {
    const s = createStorage();
    _clearIdempotencyForTests();

    const key = `test-idem-stale-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    let factoryCalls = 0;
    const factory = async () => {
      factoryCalls++;
      const t = await s.createTask({
        task: `stale-${factoryCalls}`,
        priority: 'P2',
        project: 'test-concurrency',
        effort: 'low',
      });
      tasksToCleanup.push({ storage: s, id: t.id });
      return t;
    };

    const first = await withIdempotencyKey(key, factory, (id) => s.getTask(id));
    await s.deleteTask(first.id);
    const idx = tasksToCleanup.findIndex((x) => x.id === first.id);
    if (idx >= 0) tasksToCleanup.splice(idx, 1);

    const second = await withIdempotencyKey(key, factory, (id) => s.getTask(id));
    expect(second.id).not.toBe(first.id);
    expect(factoryCalls).toBe(2);
  });

  it('failed factory does NOT get cached — retry succeeds', async () => {
    const s = createStorage();
    _clearIdempotencyForTests();

    const key = `test-idem-fail-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    let factoryCalls = 0;
    const factory = async () => {
      factoryCalls++;
      if (factoryCalls === 1) throw new Error('simulated failure');
      const t = await s.createTask({
        task: 'retry success',
        priority: 'P2',
        project: 'test-concurrency',
        effort: 'low',
      });
      tasksToCleanup.push({ storage: s, id: t.id });
      return t;
    };

    await expect(
      withIdempotencyKey(key, factory, (id) => s.getTask(id)),
    ).rejects.toThrow('simulated failure');
    const second = await withIdempotencyKey(key, factory, (id) => s.getTask(id));
    expect(second.task).toBe('retry success');
    expect(factoryCalls).toBe(2);
  });

  it('cache sizes reflect state', async () => {
    const s = createStorage();
    _clearIdempotencyForTests();
    expect(_idempotencyCacheSizes()).toEqual({ inFlight: 0, done: 0 });

    const key = `test-idem-size-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    const factory = async () => {
      const t = await s.createTask({
        task: 'size check',
        priority: 'P2',
        project: 'test-concurrency',
        effort: 'low',
      });
      tasksToCleanup.push({ storage: s, id: t.id });
      return t;
    };
    await withIdempotencyKey(key, factory, (id) => s.getTask(id));
    const sizes = _idempotencyCacheSizes();
    expect(sizes.done).toBe(1);
    expect(sizes.inFlight).toBe(0);
  });
});

describe('CONC-006 — transactional dual-write', () => {
  it('saveBoth serializes — max in-flight for each mutex stays at 1 across 50 concurrent calls', async () => {
    const s = createStorage();
    s._maxWriteInFlight.workspace = 0;
    s._maxWriteInFlight.globalML = 0;
    s._writeInFlight.workspace = 0;
    s._writeInFlight.globalML = 0;

    // 50 concurrent saveBoth calls. Each acquires workspace → globalML in
    // sequence. The outer workspace mutex serializes them all; nested
    // globalML also serializes (implicitly, since its outer lock is also
    // held). Max observed = 1 for each.
    await Promise.all(Array.from({ length: 50 }, () => s.saveBoth()));

    expect(s._maxWriteInFlight.workspace).toBe(1);
    expect(s._maxWriteInFlight.globalML).toBe(1);
    expect(s._writeInFlight.workspace).toBe(0);
    expect(s._writeInFlight.globalML).toBe(0);
  });

  it('20 concurrent createDecision calls — all decisions persist to both files', async () => {
    const s = createStorage();

    // Snapshot existing counts
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const beforeDb = ((s as any).db.decisions as unknown[]).length;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const beforeML = ((s as any).globalML.decisions as unknown[]).length;

    const marker = `conc006-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    await Promise.all(
      Array.from({ length: 20 }, (_, i) =>
        s.createDecision({
          date: new Date().toISOString().split('T')[0],
          decision: `${marker}-${i}`,
          rationale: `rationale ${i}`,
        }),
      ),
    );

    // All 20 present in both stores — no interleave dropped any
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const dbDecisions = (s as any).db.decisions as { decision: string }[];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const mlDecisions = (s as any).globalML.decisions as { decision: string }[];
    const mineInDb = dbDecisions.filter((d) => d.decision.startsWith(marker));
    const mineInMl = mlDecisions.filter((d) => d.decision.startsWith(marker));
    expect(mineInDb.length).toBe(20);
    expect(mineInMl.length).toBe(20);

    // Drift check: total counts in both arrays grew by exactly 20 each
    expect(dbDecisions.length - beforeDb).toBe(20);
    expect(mlDecisions.length - beforeML).toBe(20);
  });

  it('nested lock order is deadlock-free (workspace → globalML)', async () => {
    // If we accidentally ever acquired globalML → workspace in some code
    // path, the nested saveBoth pattern would deadlock. This test runs
    // saveBoth under a concurrent runExclusiveWorkspace to force the nested
    // acquire and verify no hang.
    const s = createStorage();

    const outer = s.runExclusiveWorkspace(async () => {
      // Inside here we hold workspace. Trying to acquire workspace again
      // (via saveBoth's outer acquire) would deadlock if not for Promise
      // scheduling isolation. saveBoth is designed to be called OUTSIDE a
      // workspace critical section — we use _saveBothFromWorkspaceContext
      // from inside. Verify that one:
      await (s as unknown as {
        _saveBothFromWorkspaceContext: () => Promise<void>;
      })._saveBothFromWorkspaceContext();
    });

    // Race a second saveBoth that acquires both from scratch.
    const other = s.saveBoth();
    await Promise.all([outer, other]);
    // If we got here, no deadlock.
    expect(true).toBe(true);
  });
});

describe('CONC-005 — heap read-safety', () => {
  // Context: the original audit flagged "getTasks reads heap while another
  // handler calls rebuildHeap" as a race. Re-analysis shows it's actually
  // safe because (a) MinHeap.toSortedArray spreads into a new array
  // synchronously (atomic at the JS microtask level), (b) rebuildHeap
  // reassigns the reference in one atomic statement, (c) writers never mutate
  // existing objects in place — they create new objects and replace
  // references. These tests document + verify that safety under stress.

  it('1000 concurrent reads during a rebuild storm — no crashes, all responses valid arrays', async () => {
    const s = createStorage();

    // Seed enough tasks that reads have something to work with
    const seedIds = Array.from(
      { length: 5 },
      (_, i) =>
        `test-heap-seed-${Date.now()}-${i}-${Math.random().toString(36).slice(2, 8)}`,
    );
    for (const id of seedIds) tasksToCleanup.push({ storage: s, id });
    for (const id of seedIds) {
      await s.createTask({
        id,
        task: `heap-${id}`,
        priority: 'P2',
        project: 'test-concurrency',
        effort: 'low',
      });
    }

    // Interleave: 1000 reads racing against 100 recalculates. If the heap
    // were truly racy we'd see undefined entries, crashes, or non-array
    // return values somewhere in this storm.
    const reads = Array.from({ length: 1000 }, () => s.getTasks());
    const writes = Array.from({ length: 100 }, () =>
      s.recalculateAllPriorities(),
    );
    const all = await Promise.all([...reads, ...writes]);

    // All reads returned arrays. Each array contains only valid task objects
    // (every entry has id + priorityScore — no nulls, no torn state).
    for (let i = 0; i < reads.length; i++) {
      const result = all[i];
      expect(Array.isArray(result)).toBe(true);
      const arr = result as unknown[];
      for (const entry of arr) {
        const t = entry as { id?: unknown; priorityScore?: unknown };
        expect(typeof t.id).toBe('string');
        expect(typeof t.priorityScore).toBe('number');
      }
    }
  });

  it('getTopPriority returns a defensive copy — mutating the returned object does not corrupt internal state', async () => {
    const s = createStorage();
    const id = `test-heap-copy-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    tasksToCleanup.push({ storage: s, id });

    await s.createTask({
      id,
      task: 'defensive copy',
      priority: 'P0',
      project: 'test-concurrency',
      effort: 'low',
      // Deliberately craft this so it's very likely the top priority for the test
      deadline: new Date(Date.now() + 1000).toISOString(),
    });

    const top1 = await s.getTopPriority();
    expect(top1).toBeDefined();
    if (!top1) return;

    // Mutate the returned object — heap internal state should be unaffected
    // because we returned a spread copy.
    const originalTitle = top1.task;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (top1 as any).task = 'MUTATED BY CALLER';

    const top2 = await s.getTopPriority();
    // The second read sees the UN-mutated internal state
    expect(top2?.task).toBe(originalTitle);
  });
});

describe('CONC-004 — atomic claimTask', () => {
  it('claim on a fresh task succeeds: sets status, claimedBy, startedAt, bumps version', async () => {
    const s = createStorage();
    const id = `test-claim-fresh-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    tasksToCleanup.push({ storage: s, id });

    await s.createTask({
      id,
      task: 'claim me',
      priority: 'P1',
      project: 'test-concurrency',
      effort: 'low',
    });
    const claimed = await s.claimTask(id, 'agent-alpha');
    expect(claimed?.status).toBe('in_progress');
    expect(claimed?.claimedBy).toBe('agent-alpha');
    expect(claimed?.startedAt).toBeDefined();
    expect(claimed?.version).toBe(2); // was 1 after create, now 2 after claim
  });

  it('re-claim by the same agent is idempotent (no-op, returns current task)', async () => {
    const s = createStorage();
    const id = `test-claim-idem-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    tasksToCleanup.push({ storage: s, id });

    await s.createTask({
      id,
      task: 'claim me twice',
      priority: 'P1',
      project: 'test-concurrency',
      effort: 'low',
    });
    const first = await s.claimTask(id, 'agent-alpha');
    const second = await s.claimTask(id, 'agent-alpha');
    expect(first?.version).toBe(2);
    expect(second?.version).toBe(2); // no additional bump on idempotent re-claim
    expect(second?.claimedBy).toBe('agent-alpha');
    expect(second?.status).toBe('in_progress');
  });

  it('claim by different agent when already claimed → TaskAlreadyClaimedError', async () => {
    const s = createStorage();
    const id = `test-claim-conflict-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    tasksToCleanup.push({ storage: s, id });

    await s.createTask({
      id,
      task: 'mine mine',
      priority: 'P1',
      project: 'test-concurrency',
      effort: 'low',
    });
    await s.claimTask(id, 'agent-alpha');
    await expect(s.claimTask(id, 'agent-bravo')).rejects.toBeInstanceOf(
      TaskAlreadyClaimedError,
    );

    try {
      await s.claimTask(id, 'agent-bravo');
    } catch (e) {
      expect(e).toBeInstanceOf(TaskAlreadyClaimedError);
      const err = e as TaskAlreadyClaimedError;
      expect(err.taskId).toBe(id);
      expect(err.claimedBy).toBe('agent-alpha');
      expect(err.status).toBe(409);
      expect(err.code).toBe('TASK_ALREADY_CLAIMED');
    }
  });

  it('claim on completed/cancelled task → TaskNotClaimableError', async () => {
    const s = createStorage();
    const id = `test-claim-done-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    tasksToCleanup.push({ storage: s, id });

    await s.createTask({
      id,
      task: 'done task',
      priority: 'P1',
      project: 'test-concurrency',
      effort: 'low',
    });
    await s.completeTask(id, 'completed');
    await expect(s.claimTask(id, 'agent-alpha')).rejects.toBeInstanceOf(
      TaskNotClaimableError,
    );
  });

  it('claim of non-existent task returns null (caller maps to 404)', async () => {
    const s = createStorage();
    const missingId = `test-claim-missing-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    const claimed = await s.claimTask(missingId, 'agent-alpha');
    expect(claimed).toBeNull();
  });

  it('10 concurrent claims by different agents → exactly 1 succeeds, 9 conflicts', async () => {
    const s = createStorage();
    const id = `test-claim-race-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    tasksToCleanup.push({ storage: s, id });

    await s.createTask({
      id,
      task: 'everyone wants me',
      priority: 'P0',
      project: 'test-concurrency',
      effort: 'low',
    });

    // Fire 10 concurrent claims from distinct agents. The mutex serializes
    // them; whoever lands the first runExclusive wins, the other 9 see
    // status=in_progress with a different claimedBy and throw.
    const results = await Promise.allSettled(
      Array.from({ length: 10 }, (_, i) => s.claimTask(id, `agent-${i}`)),
    );

    const fulfilled = results.filter((r) => r.status === 'fulfilled');
    const rejected = results.filter((r) => r.status === 'rejected');
    expect(fulfilled.length).toBe(1);
    expect(rejected.length).toBe(9);
    for (const r of rejected) {
      expect((r as PromiseRejectedResult).reason).toBeInstanceOf(
        TaskAlreadyClaimedError,
      );
    }

    // Winner is reflected in storage
    const final = await s.getTask(id);
    expect(final?.status).toBe('in_progress');
    expect(final?.claimedBy).toMatch(/^agent-\d$/);
    // Version bumped exactly once (from 1 to 2) despite 10 claim attempts
    expect(final?.version).toBe(2);
  });
});
