import { describe, expect, it } from 'vitest';
import {
  minBranchOnly,
  behindBaseBlocker,
  withGitignoreLine,
  decideSnapshotAction,
  rollbackRemovals,
  staleHistoryAdvisory,
} from '../squash-policy.js';

describe('minBranchOnly — threshold by mode', () => {
  it('squash needs at least 2 branch-only migrations', () => {
    expect(minBranchOnly('squash')).toBe(2);
  });
  it('rebase-snapshot acts on a single branch-only migration (its job is to repair the snapshot)', () => {
    expect(minBranchOnly('rebase-snapshot')).toBe(1);
  });
});

describe('behindBaseBlocker — refuse when behind the reference (data-loss guard)', () => {
  const base = { currentBranch: 'feature/x', baseRef: 'origin/develop', mode: 'squash' as const };

  it('is safe (null) when in sync', () => {
    expect(behindBaseBlocker({ ...base, behindBase: 0, allowBehindBase: false })).toBeNull();
  });

  it('blocks when behind and not overridden', () => {
    const msg = behindBaseBlocker({ ...base, behindBase: 3, allowBehindBase: false });
    expect(msg).toBeTruthy();
    expect(msg).toContain('3 commit(s) behind');
    expect(msg).toContain('origin/develop');
    expect(msg).toContain('allowBehindBase');
  });

  it('is overridable with allowBehindBase', () => {
    expect(behindBaseBlocker({ ...base, behindBase: 3, allowBehindBase: true })).toBeNull();
  });

  it('uses the rebase-snapshot verb in its message', () => {
    const msg = behindBaseBlocker({ ...base, mode: 'rebase-snapshot', behindBase: 1, allowBehindBase: false });
    expect(msg).toContain('rebase-snapshot now');
  });

  it('treats a negative count as safe (defensive)', () => {
    expect(behindBaseBlocker({ ...base, behindBase: -1, allowBehindBase: false })).toBeNull();
  });
});

describe('decideSnapshotAction — empty-base snapshot handling', () => {
  it('resets to base when the reference carries a snapshot (common case)', () => {
    expect(decideSnapshotAction({ baseHasSnapshot: true, basePreserved: 3 })).toBe('reset-to-base');
  });

  it('deletes the snapshot for an empty base (no snapshot AND no inherited migrations) → full InitialCreate', () => {
    expect(decideSnapshotAction({ baseHasSnapshot: false, basePreserved: 0 })).toBe('delete-for-initial-create');
  });

  it('NEVER deletes when migrations are inherited but the snapshot is missing (malformed base → reset, fails loudly)', () => {
    // Defensive: basePreserved > 0 means inherited history exists; deleting the
    // snapshot would drop it. Fall through to reset-to-base instead.
    expect(decideSnapshotAction({ baseHasSnapshot: false, basePreserved: 2 })).toBe('reset-to-base');
  });

  it('resets to base when a snapshot exists even with zero inherited migrations', () => {
    expect(decideSnapshotAction({ baseHasSnapshot: true, basePreserved: 0 })).toBe('reset-to-base');
  });
});

describe('rollbackRemovals — files the failed run created (present now, absent from backup)', () => {
  const MIG = 'src/App.Infrastructure/Persistence/Migrations';

  it('targets only the new consolidated migration EF wrote, keeping pre-run files', () => {
    const backedUp = [`${MIG}/001_A.cs`, `${MIG}/001_A.Designer.cs`, `${MIG}/ModelSnapshot.cs`];
    const now = [
      `${MIG}/001_A.cs`,
      `${MIG}/001_A.Designer.cs`,
      `${MIG}/ModelSnapshot.cs`,
      `${MIG}/20260603_Consolidated.cs`,
      `${MIG}/20260603_Consolidated.Designer.cs`,
    ];
    expect(rollbackRemovals(now, backedUp)).toEqual([
      `${MIG}/20260603_Consolidated.cs`,
      `${MIG}/20260603_Consolidated.Designer.cs`,
    ]);
  });

  it('removes nothing when the run failed before creating any file (e.g. snapshot reset failed)', () => {
    const files = [`${MIG}/ModelSnapshot.cs`];
    // Deleted branch-only files are absent from disk now but present in the
    // backup → they are restored by copy, never listed for removal.
    expect(rollbackRemovals(files, [...files, `${MIG}/001_A.cs`])).toEqual([]);
  });

  it('returns an empty list when disk already matches the backup', () => {
    const files = [`${MIG}/001_A.cs`, `${MIG}/ModelSnapshot.cs`];
    expect(rollbackRemovals(files, files)).toEqual([]);
  });
});

describe('withGitignoreLine — idempotent .gitignore management', () => {
  const LINE = '.efcore-squash-backup/';

  it('creates the line when the file is absent (null)', () => {
    expect(withGitignoreLine(null, LINE)).toBe('.efcore-squash-backup/\n');
  });

  it('appends to existing content, preserving it and keeping one trailing newline', () => {
    expect(withGitignoreLine('bin/\nobj/\n', LINE)).toBe('bin/\nobj/\n.efcore-squash-backup/\n');
  });

  it('appends cleanly when the file has no trailing newline', () => {
    expect(withGitignoreLine('bin/', LINE)).toBe('bin/\n.efcore-squash-backup/\n');
  });

  it('returns null when the exact line already exists', () => {
    expect(withGitignoreLine('bin/\n.efcore-squash-backup/\n', LINE)).toBeNull();
  });

  it('treats trailing-slash variants as already present', () => {
    expect(withGitignoreLine('.efcore-squash-backup\n', LINE)).toBeNull();
  });

  it('is idempotent — applying twice changes nothing the second time', () => {
    const once = withGitignoreLine('node_modules/\n', LINE);
    expect(once).not.toBeNull();
    expect(withGitignoreLine(once, LINE)).toBeNull();
  });
});

describe('staleHistoryAdvisory — warn the dev DB history is stale after a consolidation', () => {
  it('returns the advisory when an assembly consolidated (success + new migration + replaced ≥1)', () => {
    const msg = staleHistoryAdvisory([
      { success: true, newMigrationName: 'ext_v0_1_0_001_GafConsolidated', deletedFiles: ['a.cs', 'b.cs', 'c.cs'] },
    ]);
    expect(msg).toBeTruthy();
    expect(msg).toContain('3 migration file(s)');
    expect(msg).toContain('ext_v0_1_0_001_GafConsolidated');
    expect(msg).toContain('a single consolidated migration');
    expect(msg).toContain('__EFMigrationsHistory');
    expect(msg).toContain('reseed');
  });

  it('sums replaced files and lists every consolidated migration across assemblies', () => {
    const msg = staleHistoryAdvisory([
      { success: true, newMigrationName: 'core_x', deletedFiles: ['a.cs'] },
      { success: true, newMigrationName: 'ext_y', deletedFiles: ['b.cs', 'c.cs'] },
    ]);
    expect(msg).toContain('3 migration file(s)');
    expect(msg).toContain('core_x, ext_y');
    expect(msg).toContain('consolidated migrations');
  });

  it('ignores assemblies that did not consolidate when summing (mixed results)', () => {
    const msg = staleHistoryAdvisory([
      { success: true, newMigrationName: 'ext_y', deletedFiles: ['b.cs', 'c.cs'] },
      { success: false, newMigrationName: null, deletedFiles: ['x.cs'] },
      { success: true, newMigrationName: 'noop', deletedFiles: [] },
    ]);
    expect(msg).toContain('2 migration file(s)');
    expect(msg).toContain('ext_y');
    expect(msg).not.toContain('noop');
  });

  it('is null when nothing was actually replaced (no-op consolidation)', () => {
    expect(staleHistoryAdvisory([{ success: true, newMigrationName: 'x', deletedFiles: [] }])).toBeNull();
  });

  it('is null when the run failed', () => {
    expect(staleHistoryAdvisory([{ success: false, newMigrationName: null, deletedFiles: ['a.cs'] }])).toBeNull();
  });

  it('is null for an empty result set', () => {
    expect(staleHistoryAdvisory([])).toBeNull();
  });
});
