package cli

import (
	"bytes"
	"testing"

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

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, "1.0.0")
	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", 0755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello world"), 0644))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file2.txt", []byte("test content"), 0644))

	opts := testOpts([]string{"mfer", "-q", "generate", "-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", 0755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello world"), 0644))
	require.NoError(t, afero.WriteFile(fs, "/testdir/subdir/file2.txt", []byte("test content"), 0644))

	// Generate manifest
	opts := testOpts([]string{"mfer", "-q", "generate", "-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", "-q", "check", "--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", 0755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello world"), 0644))

	// Generate manifest
	opts := testOpts([]string{"mfer", "-q", "generate", "-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", "-q", "check", "--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", 0755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello world"), 0644))

	// Generate manifest
	opts := testOpts([]string{"mfer", "-q", "generate", "-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"), 0644))

	// Check manifest - should fail with hash mismatch
	opts = testOpts([]string{"mfer", "-q", "check", "--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", 0755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello world"), 0644))

	// Generate manifest
	opts := testOpts([]string{"mfer", "-q", "generate", "-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"), 0644))

	// Check manifest - should fail with size mismatch
	opts = testOpts([]string{"mfer", "-q", "check", "--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", 0755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0644))

	// 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 TestGenerateWithIgnoreDotfiles(t *testing.T) {
	fs := afero.NewMemMapFs()

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

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

	// Check that manifest exists and we can verify (hidden file won't cause failure even if missing)
	exists, _ := afero.Exists(fs, "/testdir/test.mf")
	assert.True(t, exists)
}

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

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

	// Generate manifest from multiple paths
	opts := testOpts([]string{"mfer", "-q", "generate", "-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", 0755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0644))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file2.txt", []byte("world"), 0644))

	// Generate manifest
	opts := testOpts([]string{"mfer", "-q", "generate", "-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", "-q", "check", "--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", 0755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0644))

	// Generate manifest
	opts := testOpts([]string{"mfer", "-q", "generate", "-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"), 0644))

	// Check with --no-extra-files (should fail - extra file exists)
	opts = testOpts([]string{"mfer", "-q", "check", "--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", 0755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0644))
	require.NoError(t, afero.WriteFile(fs, "/testdir/subdir/file2.txt", []byte("world"), 0644))

	// Generate manifest
	opts := testOpts([]string{"mfer", "-q", "generate", "-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"), 0644))

	// Check with --no-extra-files (should fail)
	opts = testOpts([]string{"mfer", "-q", "check", "--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", 0755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0644))

	// Generate manifest
	opts := testOpts([]string{"mfer", "-q", "generate", "-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"), 0644))

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