// shared-wal.test.ts — Tests for shared WAL DB path resolution and concurrent writes.
// Verifies: resolveProjectRootDbPath() for worktree/root paths, WAL concurrent writes.

import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
import { join, sep } from 'node:path';
import { tmpdir } from 'node:os';

import { resolveProjectRootDbPath } from '../bootstrap/dynamic-tools.ts';
import {
  openDatabase,
  closeDatabase,
  transaction,
  insertMilestone,
  getAllMilestones,
  _getAdapter,
} from '../sf-db.ts';
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';


// ─── Helpers ──────────────────────────────────────────────────────────────

function createTmpDir(suffix: string): string {
  return mkdtempSync(join(tmpdir(), `sf-wal-${suffix}-`));
}

function cleanup(dir: string): void {
  rmSync(dir, { recursive: true, force: true });
}

// ─── Tests ────────────────────────────────────────────────────────────────

describe('shared-wal', async () => {
  // ─── Test (a): resolveProjectRootDbPath returns project root DB for worktree path ───
  console.log('\n=== shared-wal: resolve worktree path to project root DB ===');
  {
    const projectRoot = '/home/user/myproject';
    const worktreePath = join(projectRoot, '.sf', 'worktrees', 'M001');
    const result = resolveProjectRootDbPath(worktreePath);
    assert.deepStrictEqual(result, join(projectRoot, '.sf', 'sf.db'),
      'worktree path resolves to project root DB');
  }

  // ─── Test (b): resolveProjectRootDbPath returns same base for project root ────
  console.log('\n=== shared-wal: resolve project root path ===');
  {
    const projectRoot = '/home/user/myproject';
    const result = resolveProjectRootDbPath(projectRoot);
    assert.deepStrictEqual(result, join(projectRoot, '.sf', 'sf.db'),
      'project root path stays at project root DB');
  }

  // ─── Test (c): resolve nested worktree subdir ──────────────────────────
  console.log('\n=== shared-wal: resolve nested worktree subdir ===');
  {
    const projectRoot = '/home/user/myproject';
    const nestedPath = join(projectRoot, '.sf', 'worktrees', 'M002', 'src', 'lib');
    const result = resolveProjectRootDbPath(nestedPath);
    assert.deepStrictEqual(result, join(projectRoot, '.sf', 'sf.db'),
      'nested worktree subdir resolves to project root DB');
  }

  // ─── Test (d): resolve with forward slashes (cross-platform) ──────────
  console.log('\n=== shared-wal: resolve forward-slash path ===');
  {
    const result = resolveProjectRootDbPath('/proj/.sf/worktrees/M001');
    assert.deepStrictEqual(result, join('/proj', '.sf', 'sf.db'),
      'forward-slash worktree path resolves correctly');
  }

  // ─── Test (e1): external-state worktree resolves to project state DB (#2952) ───
  console.log('\n=== shared-wal: resolve external-state worktree path (#2952) ===');
  {
    // External-state layout: ~/.sf/projects/<hash>/worktrees/<MID>
    // Should resolve to:     ~/.sf/projects/<hash>/sf.db
    const stateRoot = '/home/user/.sf/projects/a1b2c3d4';
    const worktreePath = join(stateRoot, 'worktrees', 'M002');
    const result = resolveProjectRootDbPath(worktreePath);
    assert.deepStrictEqual(result, join(stateRoot, 'sf.db'),
      'external-state worktree path resolves to project state DB (#2952)');
  }

  // ─── Test (e2): external-state worktree nested subdir (#2952) ─────────
  console.log('\n=== shared-wal: resolve external-state worktree nested subdir (#2952) ===');
  {
    const stateRoot = '/home/user/.sf/projects/deadbeef42';
    const nestedPath = join(stateRoot, 'worktrees', 'M003', 'src', 'lib');
    const result = resolveProjectRootDbPath(nestedPath);
    assert.deepStrictEqual(result, join(stateRoot, 'sf.db'),
      'external-state nested worktree subdir resolves to project state DB (#2952)');
  }

  // ─── Test (e3): external-state worktree with forward slashes (#2952) ──
  console.log('\n=== shared-wal: resolve external-state worktree forward-slash (#2952) ===');
  {
    const result = resolveProjectRootDbPath('/Users/dev/.sf/projects/cafe0123/worktrees/M001');
    assert.deepStrictEqual(result, join('/Users/dev/.sf/projects/cafe0123', 'sf.db'),
      'external-state forward-slash worktree path resolves correctly (#2952)');
  }

  // ─── Test (e): Concurrent writes — 3 connections to same WAL DB ───────
  console.log('\n=== shared-wal: concurrent writes via WAL ===');
  {
    const tmp = createTmpDir('concurrent');
    const dbPath = join(tmp, 'test.db');
    try {
      // Open with openDatabase to init schema + WAL mode
      openDatabase(dbPath);

      // Insert milestones from the main connection
      insertMilestone({
        id: 'M001', title: 'From conn 1', status: 'active',
      });

      // Open two additional raw connections via openDatabase in separate calls.
      // Since openDatabase closes the previous connection and opens a new one,
      // we simulate concurrent access by using the transaction() wrapper to
      // verify WAL allows reads while writes are happening.

      // Write M002
      insertMilestone({
        id: 'M002', title: 'From conn 2', status: 'active',
      });

      // Write M003
      insertMilestone({
        id: 'M003', title: 'From conn 3', status: 'active',
      });

      // Verify all 3 milestones are visible
      const all = getAllMilestones();
      assert.deepStrictEqual(all.length, 3, 'concurrent: all 3 milestones visible');
      const ids = all.map(m => m.id).sort();
      assert.deepStrictEqual(ids, ['M001', 'M002', 'M003'], 'concurrent: correct IDs');

      closeDatabase();
    } finally {
      closeDatabase();
      cleanup(tmp);
    }
  }

  // ─── Test (f): WAL concurrent — multiple raw connections to file DB ────
  console.log('\n=== shared-wal: true concurrent connections via raw SQLite ===');
  {
    const tmp = createTmpDir('rawconc');
    const dbPath = join(tmp, 'concurrent.db');
    try {
      // Open first connection and init schema
      openDatabase(dbPath);
      closeDatabase();

      // To test true concurrent access, we open 3 separate raw connections
      // using the same provider. The openDatabase/closeDatabase cycle proves
      // WAL mode persists and multiple sequential openers see each other's writes.

      // Connection 1: write M001
      openDatabase(dbPath);
      insertMilestone({ id: 'M001', title: 'Writer 1', status: 'active' });
      closeDatabase();

      // Connection 2: write M002, verify sees M001
      openDatabase(dbPath);
      const afterConn2Before = getAllMilestones();
      assert.ok(afterConn2Before.some(m => m.id === 'M001'),
        'rawconc: conn2 sees M001 from conn1');
      insertMilestone({ id: 'M002', title: 'Writer 2', status: 'active' });
      closeDatabase();

      // Connection 3: write M003, verify sees M001 + M002
      openDatabase(dbPath);
      const afterConn3Before = getAllMilestones();
      assert.ok(afterConn3Before.some(m => m.id === 'M001'),
        'rawconc: conn3 sees M001');
      assert.ok(afterConn3Before.some(m => m.id === 'M002'),
        'rawconc: conn3 sees M002');
      insertMilestone({ id: 'M003', title: 'Writer 3', status: 'active' });

      // Final read: all 3 visible
      const finalAll = getAllMilestones();
      assert.deepStrictEqual(finalAll.length, 3, 'rawconc: all 3 milestones visible');
      assert.deepStrictEqual(
        finalAll.map(m => m.id).sort(),
        ['M001', 'M002', 'M003'],
        'rawconc: all IDs present',
      );

      closeDatabase();
    } finally {
      closeDatabase();
      cleanup(tmp);
    }
  }

  // ─── Test (g): BUSY retry — transaction wrapper handles contention ─────
  console.log('\n=== shared-wal: transaction rollback on error ===');
  {
    const tmp = createTmpDir('busy');
    const dbPath = join(tmp, 'busy.db');
    try {
      openDatabase(dbPath);

      // Insert a milestone in a transaction
      transaction(() => {
        insertMilestone({ id: 'M001', title: 'In txn', status: 'active' });
      });

      // Verify it committed
      const all = getAllMilestones();
      assert.deepStrictEqual(all.length, 1, 'busy: M001 committed via transaction');

      // Verify transaction rolls back on error
      let errorCaught = false;
      try {
        transaction(() => {
          insertMilestone({ id: 'M002', title: 'Will fail', status: 'active' });
          throw new Error('Simulated failure');
        });
      } catch (err) {
        errorCaught = true;
        assert.ok(
          (err as Error).message.includes('Simulated failure'),
          'busy: error propagated from transaction',
        );
      }
      assert.ok(errorCaught, 'busy: transaction threw on error');

      // M002 should NOT be visible (rolled back)
      const afterRollback = getAllMilestones();
      assert.deepStrictEqual(afterRollback.length, 1, 'busy: M002 rolled back — still only 1 milestone');
      assert.deepStrictEqual(afterRollback[0]!.id, 'M001', 'busy: only M001 survives');

      closeDatabase();
    } finally {
      closeDatabase();
      cleanup(tmp);
    }
  }
});
