/**
 * Tests for sync.ts manageGitignore() — step 8 of v0.22.3 storage tiering.
 *
 * Issue #2: function was defined but never invoked. Now wired into runSync
 * after a successful sync (skips on dry_run / blocked_by_failures / failure).
 *
 * Tests cover: happy path, idempotency, GBRAIN_NO_GITIGNORE escape hatch,
 * submodule detection, write-error graceful degradation, and the "no
 * config — no-op" path.
 */

import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync, existsSync, chmodSync, symlinkSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { manageGitignore } from '../src/commands/sync.ts';
import { __resetMissingStorageWarning } from '../src/core/storage-config.ts';

let tmp: string;
let warnings: string[];
let originalWarn: typeof console.warn;
let originalEnv: string | undefined;

beforeEach(() => {
  tmp = mkdtempSync(join(tmpdir(), 'gbrain-mgi-test-'));
  __resetMissingStorageWarning();
  warnings = [];
  originalWarn = console.warn;
  console.warn = (...args: unknown[]) => {
    warnings.push(args.map(String).join(' '));
  };
  originalEnv = process.env.GBRAIN_NO_GITIGNORE;
  delete process.env.GBRAIN_NO_GITIGNORE;
});

afterEach(() => {
  console.warn = originalWarn;
  if (originalEnv === undefined) delete process.env.GBRAIN_NO_GITIGNORE;
  else process.env.GBRAIN_NO_GITIGNORE = originalEnv;
  // Restore permissions for cleanup.
  try {
    chmodSync(tmp, 0o755);
  } catch {
    /* ignore */
  }
  rmSync(tmp, { recursive: true, force: true });
});

function writeStorageConfig(): void {
  writeFileSync(
    join(tmp, 'gbrain.yml'),
    `storage:
  db_tracked:
    - people/
  db_only:
    - media/x/
    - media/articles/
`,
  );
}

describe('manageGitignore', () => {
  test('no-op when gbrain.yml is absent', () => {
    manageGitignore(tmp);
    expect(existsSync(join(tmp, '.gitignore'))).toBe(false);
    expect(warnings).toEqual([]);
  });

  test('no-op when storage config has empty db_only', () => {
    writeFileSync(
      join(tmp, 'gbrain.yml'),
      `storage:
  db_tracked:
    - people/
  db_only: []
`,
    );
    manageGitignore(tmp);
    expect(existsSync(join(tmp, '.gitignore'))).toBe(false);
  });

  test('appends db_only directories to .gitignore — happy path', () => {
    writeStorageConfig();
    manageGitignore(tmp);
    const content = readFileSync(join(tmp, '.gitignore'), 'utf-8');
    expect(content).toContain('# Auto-managed by gbrain');
    expect(content).toContain('media/x/');
    expect(content).toContain('media/articles/');
  });

  test('idempotent — running twice does NOT duplicate entries', () => {
    writeStorageConfig();
    manageGitignore(tmp);
    manageGitignore(tmp);
    const content = readFileSync(join(tmp, '.gitignore'), 'utf-8');
    const xCount = (content.match(/^media\/x\/$/gm) || []).length;
    const articlesCount = (content.match(/^media\/articles\/$/gm) || []).length;
    expect(xCount).toBe(1);
    expect(articlesCount).toBe(1);
  });

  test('preserves user-written .gitignore entries', () => {
    writeStorageConfig();
    writeFileSync(join(tmp, '.gitignore'), '# my own rules\n*.swp\nnode_modules/\n');
    manageGitignore(tmp);
    const content = readFileSync(join(tmp, '.gitignore'), 'utf-8');
    expect(content).toContain('# my own rules');
    expect(content).toContain('*.swp');
    expect(content).toContain('node_modules/');
    expect(content).toContain('media/x/');
  });

  test('GBRAIN_NO_GITIGNORE=1 skips entirely', () => {
    writeStorageConfig();
    process.env.GBRAIN_NO_GITIGNORE = '1';
    manageGitignore(tmp);
    expect(existsSync(join(tmp, '.gitignore'))).toBe(false);
  });

  test('skips with actionable warning when repo is a git submodule', () => {
    writeStorageConfig();
    // Submodule: .git is a file containing `gitdir: ...` instead of a directory.
    writeFileSync(join(tmp, '.git'), 'gitdir: ../.git/modules/sub\n');
    manageGitignore(tmp);
    expect(existsSync(join(tmp, '.gitignore'))).toBe(false);
    expect(warnings.some((w) => /submodule/.test(w))).toBe(true);
  });

  test('proceeds when .git is a directory (regular repo)', () => {
    writeStorageConfig();
    mkdirSync(join(tmp, '.git'));
    manageGitignore(tmp);
    expect(existsSync(join(tmp, '.gitignore'))).toBe(true);
    expect(warnings.filter((w) => /submodule/.test(w))).toEqual([]);
  });

  test('warns and skips when .gitignore write fails (read-only filesystem simulation)', () => {
    writeStorageConfig();
    // Create a .gitignore as a directory — write to that path will fail with EISDIR.
    mkdirSync(join(tmp, '.gitignore'));
    manageGitignore(tmp);
    expect(warnings.some((w) => /Could not (read|update)/.test(w))).toBe(true);
  });

  // ────────────────────────────────────────────────────────────────
  // Worktree vs submodule discrimination (closes #889)
  // ────────────────────────────────────────────────────────────────

  test('REGRESSION: submodule with relative gitdir/modules/ → skip (D49 contract)', () => {
    writeStorageConfig();
    writeFileSync(join(tmp, '.git'), 'gitdir: ../.git/modules/sub\n');
    manageGitignore(tmp);
    expect(existsSync(join(tmp, '.gitignore'))).toBe(false);
    expect(warnings.some((w) => /submodule/.test(w))).toBe(true);
  });

  test('absorbed submodule with absolute gitdir/modules/ → skip (closes edge case)', () => {
    writeStorageConfig();
    // After `git submodule absorbgitdirs`, the gitdir path becomes absolute.
    writeFileSync(join(tmp, '.git'), 'gitdir: /home/user/parent/.git/modules/sub\n');
    manageGitignore(tmp);
    expect(existsSync(join(tmp, '.gitignore'))).toBe(false);
    expect(warnings.some((w) => /submodule/.test(w))).toBe(true);
  });

  test('CRITICAL: worktree with absolute gitdir/worktrees/ → MANAGE (closes #889)', () => {
    writeStorageConfig();
    writeFileSync(join(tmp, '.git'), 'gitdir: /home/user/repo/.git/worktrees/feature-branch\n');
    manageGitignore(tmp);
    expect(existsSync(join(tmp, '.gitignore'))).toBe(true);
    expect(warnings.filter((w) => /submodule/.test(w))).toEqual([]);
  });

  test('worktree with relative gitdir/worktrees/ → MANAGE', () => {
    writeStorageConfig();
    writeFileSync(join(tmp, '.git'), 'gitdir: ../.git/worktrees/feature-branch\n');
    manageGitignore(tmp);
    expect(existsSync(join(tmp, '.gitignore'))).toBe(true);
    expect(warnings.filter((w) => /submodule/.test(w))).toEqual([]);
  });

  test('malformed .git file (no gitdir: prefix) → MANAGE (preserves catch behavior)', () => {
    writeStorageConfig();
    writeFileSync(join(tmp, '.git'), 'garbage content\n');
    manageGitignore(tmp);
    // No gitdir prefix → not a submodule → MANAGE.
    expect(existsSync(join(tmp, '.gitignore'))).toBe(true);
    expect(warnings.filter((w) => /submodule/.test(w))).toEqual([]);
  });
});

// #2788: collector-output vs db_only collision warning at .gitignore-write time.
describe('manageGitignore collector/db_only collision warning (#2788)', () => {
  let recipesDir: string;
  const SECRET_ENV_KEYS = ['CLAWVISOR_URL', 'CLAWVISOR_AGENT_TOKEN', 'GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET'];
  let savedEnv: Record<string, string | undefined>;

  beforeEach(() => {
    recipesDir = mkdtempSync(join(tmpdir(), 'gbrain-recipes-'));
    savedEnv = {};
    // Make embedded recipes (calendar-to-brain) deterministically unconfigured
    // and point recipe discovery at our temp dir.
    for (const k of [...SECRET_ENV_KEYS, 'GBRAIN_RECIPES_DIR', 'GBRAIN_HOME']) {
      savedEnv[k] = process.env[k];
    }
    for (const k of SECRET_ENV_KEYS) delete process.env[k];
    process.env.GBRAIN_RECIPES_DIR = recipesDir;
    process.env.GBRAIN_HOME = recipesDir; // heartbeat reads stay hermetic
    writeFileSync(
      join(recipesDir, 'test-collector.md'),
      '---\nid: test-collector\nname: Test Collector\noutput_paths:\n  - media/x/inbox/\n---\nBody.\n',
    );
  });

  afterEach(() => {
    for (const [k, v] of Object.entries(savedEnv)) {
      if (v === undefined) delete process.env[k];
      else process.env[k] = v;
    }
    rmSync(recipesDir, { recursive: true, force: true });
  });

  test('warns when a configured collector output dir sits inside a db_only path', () => {
    writeStorageConfig(); // db_only includes media/x/
    manageGitignore(tmp);
    const hit = warnings.find((w) => /collector 'test-collector'/.test(w));
    expect(hit).toBeDefined();
    expect(hit).toContain("'media/x/inbox/'");
    expect(hit).toContain("db_only path 'media/x/'");
    expect(hit).toContain('silently skip');
    // .gitignore management still happens — the warning never blocks it.
    expect(existsSync(join(tmp, '.gitignore'))).toBe(true);
  });

  test('no warning when the collector writes outside every db_only path', () => {
    writeFileSync(join(tmp, 'gbrain.yml'), 'storage:\n  db_only:\n    - archive/\n');
    manageGitignore(tmp);
    expect(warnings.filter((w) => /collector 'test-collector'/.test(w))).toEqual([]);
  });
});
