/**
 * setupPath profile management (issue #1327, MOSAIC-IMPROVEMENTS 4c / D25).
 *
 * The profile append used to be guarded on the binDir value it was about to
 * write, which is blind to accumulation across different Mosaic homes: every
 * wizard run against a fresh temp home appended a permanent block to the
 * operator's real shell profile (1,061 measured appends on sb-it-1-dt).
 *
 * Arms below map to the requirements:
 *   S1 sentinel-managed block, rewritten in place
 *   S2 a non-default target home never touches the operator profile
 *   S3 byte-identical profile across repeated runs
 *   S4 legacy unmarked `# Mosaic` blocks collapse into the managed block
 *   S5 the Windows ($env:Path) arm shares the same block logic
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir, homedir } from 'node:os';

let profilePathMock: string | null = null;

vi.mock('../platform/detect.js', () => ({
  getShellProfilePath: (): string | null => profilePathMock,
}));

import { setupPath, managedBlockFor, stripLegacyPathBlocks } from './finalize.js';

// The real resolved default on this host. Tests use it as the comparator a
// non-default home must fail against, exactly as the wizard would.
const REAL_DEFAULT_HOME = join(homedir(), '.config', 'mosaic');

function tempHome(prefix: string): string {
  const dir = join(tmpdir(), prefix);
  mkdirSync(join(dir, 'bin'), { recursive: true });
  return dir;
}

describe('setupPath profile management (#1327)', () => {
  let workDir: string;
  let profileFile: string;
  let defaultLikeHome: string;
  let otherHome: string;
  const baseline = '# existing operator content\nexport EDITOR=vim\n';

  beforeEach(() => {
    workDir = mkdtempSync(join(tmpdir(), 'setuppath-spec-'));
    profileFile = join(workDir, '.bashrc');
    writeFileSync(profileFile, baseline, 'utf-8');
    profilePathMock = profileFile;
    defaultLikeHome = tempHome(join(workDir, 'home-a', '.config', 'mosaic'));
    otherHome = tempHome(join(workDir, 'home-b', '.config', 'mosaic'));
  });

  afterEach(() => {
    profilePathMock = null;
    rmSync(workDir, { recursive: true, force: true });
  });

  // S2 — the arm that MUST fail against the pre-fix code: a home that is not
  // the resolved default may not modify the operator profile at all.
  it('does not touch the operator profile when the target home is not the resolved default', () => {
    const action = setupPath(otherHome, REAL_DEFAULT_HOME);
    expect(action).toBe('skipped');
    expect(readFileSync(profileFile, 'utf-8')).toBe(baseline);
  });

  it('returns skipped when no shell profile can be resolved', () => {
    profilePathMock = null;
    const action = setupPath(defaultLikeHome, defaultLikeHome);
    expect(action).toBe('skipped');
  });

  // S1 + S3 — two distinct homes (each run as the resolved default in turn,
  // the shape of two legitimate installs against one operator profile) and
  // repeated runs against the same home both leave exactly one block.
  it('leaves exactly one managed block after runs against two distinct homes', () => {
    const first = setupPath(defaultLikeHome, defaultLikeHome);
    expect(first).toBe('added');

    const second = setupPath(otherHome, otherHome);
    expect(second).toBe('added');

    const content = readFileSync(profileFile, 'utf-8');
    const beginCount = content.split('# >>> mosaic begin >>>').length - 1;
    const endCount = content.split('# <<< mosaic end <<<').length - 1;
    expect(beginCount).toBe(1);
    expect(endCount).toBe(1);
    expect(content).toContain(join(otherHome, 'bin'));
    expect(content).toContain(baseline);
  });

  it('is byte-identical across repeated runs against the same home', () => {
    setupPath(defaultLikeHome, defaultLikeHome);
    const afterFirst = readFileSync(profileFile, 'utf-8');

    const again = setupPath(defaultLikeHome, defaultLikeHome);
    expect(again).toBe('already');
    expect(readFileSync(profileFile, 'utf-8')).toBe(afterFirst);
  });

  // S4 — pre-existing unmarked blocks from the old append logic collapse
  // into the single managed block instead of accumulating beside it.
  it('collapses legacy unmarked # Mosaic blocks into the managed block', () => {
    const legacy =
      '# existing operator content\n' +
      '# Mosaic\n' +
      'export PATH="/tmp/mosaic-dead-wizard-1/bin:$PATH"\n' +
      'export EDITOR=vim\n' +
      '# Mosaic\n' +
      'export PATH="/tmp/mosaic-dead-wizard-2/bin:$PATH"\n';
    writeFileSync(profileFile, legacy, 'utf-8');

    const action = setupPath(defaultLikeHome, defaultLikeHome);
    expect(action).toBe('added');

    const content = readFileSync(profileFile, 'utf-8');
    expect(content).not.toContain('/tmp/mosaic-dead-wizard-1/bin');
    expect(content).not.toContain('/tmp/mosaic-dead-wizard-2/bin');
    expect(content).toContain('export EDITOR=vim');
    expect(content.split('# >>> mosaic begin >>>').length - 1).toBe(1);
    expect(content).toContain(join(defaultLikeHome, 'bin'));
  });
});

describe('managed block helpers (#1327)', () => {
  // S5 — the Windows arm shares markers and shape with the POSIX arm.
  it('builds the $env:Path variant inside the same markers', () => {
    const block = managedBlockFor('C:\\Users\\op\\.config\\mosaic\\bin', true);
    expect(block).toContain('# >>> mosaic begin >>>');
    expect(block).toContain('# <<< mosaic end <<<');
    expect(block).toContain('$env:Path = "C:\\Users\\op\\.config\\mosaic\\bin;$env:Path"');
  });

  it('builds the POSIX export variant inside the same markers', () => {
    const block = managedBlockFor('/home/op/.config/mosaic/bin', false);
    expect(block).toContain('# >>> mosaic begin >>>');
    expect(block).toContain('export PATH="/home/op/.config/mosaic/bin:$PATH"');
    expect(block).toContain('# <<< mosaic end <<<');
  });

  it('strips legacy $env:Path pairs on the Windows arm', () => {
    const legacy =
      '# Mosaic\n$env:Path = "C:\\tmp\\dead\\bin;$env:Path"\n' +
      '# Mosaic\n$env:Path = "C:\\tmp\\dead2\\bin;$env:Path"\n' +
      'Write-Host hi\n';
    const stripped = stripLegacyPathBlocks(legacy, true);
    expect(stripped).not.toContain('C:\\tmp\\dead');
    expect(stripped).toContain('Write-Host hi');
  });
});
