package cli

import (
	"bytes"
	"fmt"
	"math/rand"
	"testing"

	"github.com/spf13/afero"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	urfcli "github.com/urfave/cli/v2"
	"sneak.berlin/go/mfer/mfer"
)

func init() {
	// Prevent urfave/cli from calling os.Exit during tests
	urfcli.OsExiter = func(code int) {}
}

func TestBuild(t *testing.T) {
	m := &CLIApp{}
	assert.NotNil(t, m)
}

func testOpts(args []string, fs afero.Fs) *RunOptions {
	return &RunOptions{
		Appname: "mfer",
		Version: "1.0.0",
		Gitrev:  "abc123",
		Args:    args,
		Stdin:   &bytes.Buffer{},
		Stdout:  &bytes.Buffer{},
		Stderr:  &bytes.Buffer{},
		Fs:      fs,
	}
}

func TestVersionCommand(t *testing.T) {
	fs := afero.NewMemMapFs()
	opts := testOpts([]string{"mfer", "version"}, fs)

	exitCode := RunWithOptions(opts)

	assert.Equal(t, 0, exitCode)
	stdout := opts.Stdout.(*bytes.Buffer).String()
	assert.Contains(t, stdout, mfer.Version)
	assert.Contains(t, stdout, "abc123")
}

func TestHelpCommand(t *testing.T) {
	fs := afero.NewMemMapFs()
	opts := testOpts([]string{"mfer", "--help"}, fs)

	exitCode := RunWithOptions(opts)

	assert.Equal(t, 0, exitCode)
	stdout := opts.Stdout.(*bytes.Buffer).String()
	assert.Contains(t, stdout, "generate")
	assert.Contains(t, stdout, "check")
	assert.Contains(t, stdout, "fetch")
}

func TestGenerateCommand(t *testing.T) {
	fs := afero.NewMemMapFs()

	// Create test files in memory filesystem
	require.NoError(t, fs.MkdirAll("/testdir", 0o755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello world"), 0o644))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file2.txt", []byte("test content"), 0o644))

	opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/testdir/test.mf", "/testdir"}, fs)

	exitCode := RunWithOptions(opts)

	assert.Equal(t, 0, exitCode, "stderr: %s", opts.Stderr.(*bytes.Buffer).String())

	// Verify manifest was created
	exists, err := afero.Exists(fs, "/testdir/test.mf")
	require.NoError(t, err)
	assert.True(t, exists)
}

func TestGenerateAndCheckCommand(t *testing.T) {
	fs := afero.NewMemMapFs()

	// Create test files with subdirectory
	require.NoError(t, fs.MkdirAll("/testdir/subdir", 0o755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello world"), 0o644))
	require.NoError(t, afero.WriteFile(fs, "/testdir/subdir/file2.txt", []byte("test content"), 0o644))

	// Generate manifest
	opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/testdir/test.mf", "/testdir"}, fs)
	exitCode := RunWithOptions(opts)
	require.Equal(t, 0, exitCode, "generate failed: %s", opts.Stderr.(*bytes.Buffer).String())

	// Check manifest
	opts = testOpts([]string{"mfer", "check", "-q", "--base", "/testdir", "/testdir/test.mf"}, fs)
	exitCode = RunWithOptions(opts)
	assert.Equal(t, 0, exitCode, "check failed: %s", opts.Stderr.(*bytes.Buffer).String())
}

func TestCheckCommandWithMissingFile(t *testing.T) {
	fs := afero.NewMemMapFs()

	// Create test file
	require.NoError(t, fs.MkdirAll("/testdir", 0o755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello world"), 0o644))

	// Generate manifest
	opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/testdir/test.mf", "/testdir"}, fs)
	exitCode := RunWithOptions(opts)
	require.Equal(t, 0, exitCode, "generate failed: %s", opts.Stderr.(*bytes.Buffer).String())

	// Delete the file
	require.NoError(t, fs.Remove("/testdir/file1.txt"))

	// Check manifest - should fail
	opts = testOpts([]string{"mfer", "check", "-q", "--base", "/testdir", "/testdir/test.mf"}, fs)
	exitCode = RunWithOptions(opts)
	assert.Equal(t, 1, exitCode, "check should have failed for missing file")
}

func TestCheckCommandWithCorruptedFile(t *testing.T) {
	fs := afero.NewMemMapFs()

	// Create test file
	require.NoError(t, fs.MkdirAll("/testdir", 0o755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello world"), 0o644))

	// Generate manifest
	opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/testdir/test.mf", "/testdir"}, fs)
	exitCode := RunWithOptions(opts)
	require.Equal(t, 0, exitCode, "generate failed: %s", opts.Stderr.(*bytes.Buffer).String())

	// Corrupt the file (change content but keep same size)
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("HELLO WORLD"), 0o644))

	// Check manifest - should fail with hash mismatch
	opts = testOpts([]string{"mfer", "check", "-q", "--base", "/testdir", "/testdir/test.mf"}, fs)
	exitCode = RunWithOptions(opts)
	assert.Equal(t, 1, exitCode, "check should have failed for corrupted file")
}

func TestCheckCommandWithSizeMismatch(t *testing.T) {
	fs := afero.NewMemMapFs()

	// Create test file
	require.NoError(t, fs.MkdirAll("/testdir", 0o755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello world"), 0o644))

	// Generate manifest
	opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/testdir/test.mf", "/testdir"}, fs)
	exitCode := RunWithOptions(opts)
	require.Equal(t, 0, exitCode, "generate failed: %s", opts.Stderr.(*bytes.Buffer).String())

	// Change file size
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("different size content here"), 0o644))

	// Check manifest - should fail with size mismatch
	opts = testOpts([]string{"mfer", "check", "-q", "--base", "/testdir", "/testdir/test.mf"}, fs)
	exitCode = RunWithOptions(opts)
	assert.Equal(t, 1, exitCode, "check should have failed for size mismatch")
}

func TestBannerOutput(t *testing.T) {
	fs := afero.NewMemMapFs()

	// Create test file
	require.NoError(t, fs.MkdirAll("/testdir", 0o755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0o644))

	// Run without -q to see banner
	opts := testOpts([]string{"mfer", "generate", "-o", "/testdir/test.mf", "/testdir"}, fs)
	exitCode := RunWithOptions(opts)
	assert.Equal(t, 0, exitCode)

	// Banner ASCII art should be in stdout
	stdout := opts.Stdout.(*bytes.Buffer).String()
	assert.Contains(t, stdout, "___")
	assert.Contains(t, stdout, "\\")
}

func TestUnknownCommand(t *testing.T) {
	fs := afero.NewMemMapFs()
	opts := testOpts([]string{"mfer", "unknown"}, fs)

	exitCode := RunWithOptions(opts)
	assert.Equal(t, 1, exitCode)
}

func TestGenerateExcludesDotfilesByDefault(t *testing.T) {
	fs := afero.NewMemMapFs()

	// Create test files including dotfiles
	require.NoError(t, fs.MkdirAll("/testdir", 0o755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0o644))
	require.NoError(t, afero.WriteFile(fs, "/testdir/.hidden", []byte("secret"), 0o644))

	// Generate manifest without --include-dotfiles (default excludes dotfiles)
	opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/testdir/test.mf", "/testdir"}, fs)
	exitCode := RunWithOptions(opts)
	require.Equal(t, 0, exitCode)

	// Check that manifest exists
	exists, _ := afero.Exists(fs, "/testdir/test.mf")
	assert.True(t, exists)

	// Verify manifest only has 1 file (the non-dotfile)
	manifest, err := mfer.NewManifestFromFile(fs, "/testdir/test.mf")
	require.NoError(t, err)
	assert.Len(t, manifest.Files(), 1)
	assert.Equal(t, "file1.txt", manifest.Files()[0].Path)
}

func TestGenerateWithIncludeDotfiles(t *testing.T) {
	fs := afero.NewMemMapFs()

	// Create test files including dotfiles
	require.NoError(t, fs.MkdirAll("/testdir", 0o755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0o644))
	require.NoError(t, afero.WriteFile(fs, "/testdir/.hidden", []byte("secret"), 0o644))

	// Generate manifest with --include-dotfiles
	opts := testOpts([]string{"mfer", "generate", "-q", "--include-dotfiles", "-o", "/testdir/test.mf", "/testdir"}, fs)
	exitCode := RunWithOptions(opts)
	require.Equal(t, 0, exitCode)

	// Verify manifest has 2 files (including dotfile)
	manifest, err := mfer.NewManifestFromFile(fs, "/testdir/test.mf")
	require.NoError(t, err)
	assert.Len(t, manifest.Files(), 2)
}

func TestMultipleInputPaths(t *testing.T) {
	fs := afero.NewMemMapFs()

	// Create test files in multiple directories
	require.NoError(t, fs.MkdirAll("/dir1", 0o755))
	require.NoError(t, fs.MkdirAll("/dir2", 0o755))
	require.NoError(t, afero.WriteFile(fs, "/dir1/file1.txt", []byte("content1"), 0o644))
	require.NoError(t, afero.WriteFile(fs, "/dir2/file2.txt", []byte("content2"), 0o644))

	// Generate manifest from multiple paths
	opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/output.mf", "/dir1", "/dir2"}, fs)
	exitCode := RunWithOptions(opts)
	assert.Equal(t, 0, exitCode, "stderr: %s", opts.Stderr.(*bytes.Buffer).String())

	exists, _ := afero.Exists(fs, "/output.mf")
	assert.True(t, exists)
}

func TestNoExtraFilesPass(t *testing.T) {
	fs := afero.NewMemMapFs()

	// Create test files
	require.NoError(t, fs.MkdirAll("/testdir", 0o755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0o644))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file2.txt", []byte("world"), 0o644))

	// Generate manifest
	opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/manifest.mf", "/testdir"}, fs)
	exitCode := RunWithOptions(opts)
	require.Equal(t, 0, exitCode)

	// Check with --no-extra-files (should pass - no extra files)
	opts = testOpts([]string{"mfer", "check", "-q", "--no-extra-files", "--base", "/testdir", "/manifest.mf"}, fs)
	exitCode = RunWithOptions(opts)
	assert.Equal(t, 0, exitCode)
}

func TestNoExtraFilesFail(t *testing.T) {
	fs := afero.NewMemMapFs()

	// Create test files
	require.NoError(t, fs.MkdirAll("/testdir", 0o755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0o644))

	// Generate manifest
	opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/manifest.mf", "/testdir"}, fs)
	exitCode := RunWithOptions(opts)
	require.Equal(t, 0, exitCode)

	// Add an extra file after manifest generation
	require.NoError(t, afero.WriteFile(fs, "/testdir/extra.txt", []byte("extra"), 0o644))

	// Check with --no-extra-files (should fail - extra file exists)
	opts = testOpts([]string{"mfer", "check", "-q", "--no-extra-files", "--base", "/testdir", "/manifest.mf"}, fs)
	exitCode = RunWithOptions(opts)
	assert.Equal(t, 1, exitCode, "check should fail when extra files exist")
}

func TestNoExtraFilesWithSubdirectory(t *testing.T) {
	fs := afero.NewMemMapFs()

	// Create test files with subdirectory
	require.NoError(t, fs.MkdirAll("/testdir/subdir", 0o755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0o644))
	require.NoError(t, afero.WriteFile(fs, "/testdir/subdir/file2.txt", []byte("world"), 0o644))

	// Generate manifest
	opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/manifest.mf", "/testdir"}, fs)
	exitCode := RunWithOptions(opts)
	require.Equal(t, 0, exitCode)

	// Add extra file in subdirectory
	require.NoError(t, afero.WriteFile(fs, "/testdir/subdir/extra.txt", []byte("extra"), 0o644))

	// Check with --no-extra-files (should fail)
	opts = testOpts([]string{"mfer", "check", "-q", "--no-extra-files", "--base", "/testdir", "/manifest.mf"}, fs)
	exitCode = RunWithOptions(opts)
	assert.Equal(t, 1, exitCode, "check should fail when extra files exist in subdirectory")
}

func TestCheckWithoutNoExtraFilesIgnoresExtra(t *testing.T) {
	fs := afero.NewMemMapFs()

	// Create test file
	require.NoError(t, fs.MkdirAll("/testdir", 0o755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0o644))

	// Generate manifest
	opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/manifest.mf", "/testdir"}, fs)
	exitCode := RunWithOptions(opts)
	require.Equal(t, 0, exitCode)

	// Add extra file
	require.NoError(t, afero.WriteFile(fs, "/testdir/extra.txt", []byte("extra"), 0o644))

	// Check WITHOUT --no-extra-files (should pass - extra files ignored)
	opts = testOpts([]string{"mfer", "check", "-q", "--base", "/testdir", "/manifest.mf"}, fs)
	exitCode = RunWithOptions(opts)
	assert.Equal(t, 0, exitCode, "check without --no-extra-files should ignore extra files")
}

func TestGenerateAtomicWriteNoTempFileOnSuccess(t *testing.T) {
	fs := afero.NewMemMapFs()

	// Create test file
	require.NoError(t, fs.MkdirAll("/testdir", 0o755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0o644))

	// Generate manifest
	opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/output.mf", "/testdir"}, fs)
	exitCode := RunWithOptions(opts)
	require.Equal(t, 0, exitCode)

	// Verify output file exists
	exists, err := afero.Exists(fs, "/output.mf")
	require.NoError(t, err)
	assert.True(t, exists, "output file should exist")

	// Verify temp file does NOT exist
	tmpExists, err := afero.Exists(fs, "/output.mf.tmp")
	require.NoError(t, err)
	assert.False(t, tmpExists, "temp file should not exist after successful generation")
}

func TestGenerateAtomicWriteOverwriteWithForce(t *testing.T) {
	fs := afero.NewMemMapFs()

	// Create test file
	require.NoError(t, fs.MkdirAll("/testdir", 0o755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0o644))

	// Create existing manifest with different content
	require.NoError(t, afero.WriteFile(fs, "/output.mf", []byte("old content"), 0o644))

	// Generate manifest with --force
	opts := testOpts([]string{"mfer", "generate", "-q", "-f", "-o", "/output.mf", "/testdir"}, fs)
	exitCode := RunWithOptions(opts)
	require.Equal(t, 0, exitCode)

	// Verify output file exists and was overwritten
	content, err := afero.ReadFile(fs, "/output.mf")
	require.NoError(t, err)
	assert.NotEqual(t, "old content", string(content), "manifest should be overwritten")

	// Verify temp file does NOT exist
	tmpExists, err := afero.Exists(fs, "/output.mf.tmp")
	require.NoError(t, err)
	assert.False(t, tmpExists, "temp file should not exist after successful generation")
}

func TestGenerateFailsWithoutForceWhenOutputExists(t *testing.T) {
	fs := afero.NewMemMapFs()

	// Create test file
	require.NoError(t, fs.MkdirAll("/testdir", 0o755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0o644))

	// Create existing manifest
	require.NoError(t, afero.WriteFile(fs, "/output.mf", []byte("existing"), 0o644))

	// Generate manifest WITHOUT --force (should fail)
	opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/output.mf", "/testdir"}, fs)
	exitCode := RunWithOptions(opts)
	assert.Equal(t, 1, exitCode, "should fail when output exists without --force")

	// Verify original content is preserved
	content, err := afero.ReadFile(fs, "/output.mf")
	require.NoError(t, err)
	assert.Equal(t, "existing", string(content), "original file should be preserved")
}

func TestGenerateAtomicWriteUsesTemp(t *testing.T) {
	// This test verifies that generate uses a temp file by checking
	// that the output file doesn't exist until generation completes.
	// We do this by generating to a path and verifying the temp file
	// pattern is used (output.mf.tmp -> output.mf)
	fs := afero.NewMemMapFs()

	// Create test file
	require.NoError(t, fs.MkdirAll("/testdir", 0o755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0o644))

	// Generate manifest
	opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/output.mf", "/testdir"}, fs)
	exitCode := RunWithOptions(opts)
	require.Equal(t, 0, exitCode)

	// Both output file should exist and temp should not
	exists, _ := afero.Exists(fs, "/output.mf")
	assert.True(t, exists, "output file should exist")

	tmpExists, _ := afero.Exists(fs, "/output.mf.tmp")
	assert.False(t, tmpExists, "temp file should be cleaned up")

	// Verify manifest is valid (not empty)
	content, err := afero.ReadFile(fs, "/output.mf")
	require.NoError(t, err)
	assert.True(t, len(content) > 0, "manifest should not be empty")
}

// failingWriterFs wraps a filesystem and makes writes fail after N bytes
type failingWriterFs struct {
	afero.Fs
	failAfter int64
	written   int64
}

type failingFile struct {
	afero.File
	fs *failingWriterFs
}

func (f *failingFile) Write(p []byte) (int, error) {
	f.fs.written += int64(len(p))
	if f.fs.written > f.fs.failAfter {
		return 0, fmt.Errorf("simulated write failure")
	}
	return f.File.Write(p)
}

func (fs *failingWriterFs) Create(name string) (afero.File, error) {
	f, err := fs.Fs.Create(name)
	if err != nil {
		return nil, err
	}
	return &failingFile{File: f, fs: fs}, nil
}

func TestGenerateAtomicWriteCleansUpOnError(t *testing.T) {
	baseFs := afero.NewMemMapFs()

	// Create test files - need enough content to trigger the write failure
	require.NoError(t, baseFs.MkdirAll("/testdir", 0o755))
	require.NoError(t, afero.WriteFile(baseFs, "/testdir/file1.txt", []byte("hello world this is a test file"), 0o644))

	// Wrap with failing writer that fails after writing some bytes
	fs := &failingWriterFs{Fs: baseFs, failAfter: 10}

	// Generate manifest - should fail during write
	opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/output.mf", "/testdir"}, fs)
	exitCode := RunWithOptions(opts)
	assert.Equal(t, 1, exitCode, "should fail due to write error")

	// With atomic writes: output.mf should NOT exist (temp was cleaned up)
	// With non-atomic writes: output.mf WOULD exist (partial/empty)
	exists, _ := afero.Exists(baseFs, "/output.mf")
	assert.False(t, exists, "output file should not exist after failed generation (atomic write)")

	// Temp file should also not exist
	tmpExists, _ := afero.Exists(baseFs, "/output.mf.tmp")
	assert.False(t, tmpExists, "temp file should be cleaned up after failed generation")
}

func TestGenerateValidatesInputPaths(t *testing.T) {
	fs := afero.NewMemMapFs()

	// Create one valid directory
	require.NoError(t, fs.MkdirAll("/validdir", 0o755))
	require.NoError(t, afero.WriteFile(fs, "/validdir/file.txt", []byte("content"), 0o644))

	t.Run("nonexistent path fails fast", func(t *testing.T) {
		opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/output.mf", "/nonexistent"}, fs)
		exitCode := RunWithOptions(opts)
		assert.Equal(t, 1, exitCode)
		stderr := opts.Stderr.(*bytes.Buffer).String()
		assert.Contains(t, stderr, "path does not exist")
		assert.Contains(t, stderr, "/nonexistent")
	})

	t.Run("mix of valid and invalid paths fails fast", func(t *testing.T) {
		opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/output.mf", "/validdir", "/alsononexistent"}, fs)
		exitCode := RunWithOptions(opts)
		assert.Equal(t, 1, exitCode)
		stderr := opts.Stderr.(*bytes.Buffer).String()
		assert.Contains(t, stderr, "path does not exist")
		assert.Contains(t, stderr, "/alsononexistent")

		// Output file should not have been created
		exists, _ := afero.Exists(fs, "/output.mf")
		assert.False(t, exists, "output file should not exist when path validation fails")
	})

	t.Run("valid paths succeed", func(t *testing.T) {
		opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/output.mf", "/validdir"}, fs)
		exitCode := RunWithOptions(opts)
		assert.Equal(t, 0, exitCode)
	})
}

func TestCheckDetectsManifestCorruption(t *testing.T) {
	fs := afero.NewMemMapFs()
	rng := rand.New(rand.NewSource(42))

	// Create many small files with random names to generate a ~1MB manifest
	// Each manifest entry is roughly 50-60 bytes, so we need ~20000 files
	require.NoError(t, fs.MkdirAll("/testdir", 0o755))

	numFiles := 20000
	for i := 0; i < numFiles; i++ {
		// Generate random filename
		filename := fmt.Sprintf("/testdir/%08x%08x%08x.dat", rng.Uint32(), rng.Uint32(), rng.Uint32())
		// Small random content
		content := make([]byte, 16+rng.Intn(48))
		rng.Read(content)
		require.NoError(t, afero.WriteFile(fs, filename, content, 0o644))
	}

	// Generate manifest outside of testdir
	opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/manifest.mf", "/testdir"}, fs)
	exitCode := RunWithOptions(opts)
	require.Equal(t, 0, exitCode, "generate should succeed")

	// Read the valid manifest and verify it's approximately 1MB
	validManifest, err := afero.ReadFile(fs, "/manifest.mf")
	require.NoError(t, err)
	require.True(t, len(validManifest) >= 1024*1024, "manifest should be at least 1MB, got %d bytes", len(validManifest))
	t.Logf("manifest size: %d bytes (%d files)", len(validManifest), numFiles)

	// First corruption: truncate the manifest
	require.NoError(t, afero.WriteFile(fs, "/manifest.mf", validManifest[:len(validManifest)/2], 0o644))

	// Check should fail with truncated manifest
	opts = testOpts([]string{"mfer", "check", "-q", "--base", "/testdir", "/manifest.mf"}, fs)
	exitCode = RunWithOptions(opts)
	assert.Equal(t, 1, exitCode, "check should fail with truncated manifest")

	// Verify check passes with valid manifest
	require.NoError(t, afero.WriteFile(fs, "/manifest.mf", validManifest, 0o644))
	opts = testOpts([]string{"mfer", "check", "-q", "--base", "/testdir", "/manifest.mf"}, fs)
	exitCode = RunWithOptions(opts)
	require.Equal(t, 0, exitCode, "check should pass with valid manifest")

	// Now do 500 random corruption iterations
	for i := 0; i < 500; i++ {
		// Corrupt: write a random byte at a random offset
		corrupted := make([]byte, len(validManifest))
		copy(corrupted, validManifest)

		offset := rng.Intn(len(corrupted))
		originalByte := corrupted[offset]
		// Make sure we actually change the byte
		newByte := byte(rng.Intn(256))
		for newByte == originalByte {
			newByte = byte(rng.Intn(256))
		}
		corrupted[offset] = newByte

		require.NoError(t, afero.WriteFile(fs, "/manifest.mf", corrupted, 0o644))

		// Check should fail with corrupted manifest
		opts = testOpts([]string{"mfer", "check", "-q", "--base", "/testdir", "/manifest.mf"}, fs)
		exitCode = RunWithOptions(opts)
		assert.Equal(t, 1, exitCode, "iteration %d: check should fail with corrupted manifest (offset %d, 0x%02x -> 0x%02x)",
			i, offset, originalByte, newByte)

		// Restore valid manifest for next iteration
		require.NoError(t, afero.WriteFile(fs, "/manifest.mf", validManifest, 0o644))
	}
}
