package mfer

import (
	"bytes"
	"context"
	"testing"
	"time"

	"github.com/spf13/afero"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestStatusString(t *testing.T) {
	tests := []struct {
		status   Status
		expected string
	}{
		{StatusOK, "OK"},
		{StatusMissing, "MISSING"},
		{StatusSizeMismatch, "SIZE_MISMATCH"},
		{StatusHashMismatch, "HASH_MISMATCH"},
		{StatusExtra, "EXTRA"},
		{StatusError, "ERROR"},
		{Status(99), "UNKNOWN"},
	}

	for _, tt := range tests {
		t.Run(tt.expected, func(t *testing.T) {
			assert.Equal(t, tt.expected, tt.status.String())
		})
	}
}

// createTestManifest creates a manifest file in the filesystem with the given files.
func createTestManifest(t *testing.T, fs afero.Fs, manifestPath string, files map[string][]byte) {
	t.Helper()

	builder := NewBuilder()
	for path, content := range files {
		reader := bytes.NewReader(content)
		_, err := builder.AddFile(RelFilePath(path), FileSize(len(content)), ModTime(time.Now()), reader, nil)
		require.NoError(t, err)
	}

	var buf bytes.Buffer
	require.NoError(t, builder.Build(&buf))
	require.NoError(t, afero.WriteFile(fs, manifestPath, buf.Bytes(), 0o644))
}

// createFilesOnDisk creates the given files on the filesystem.
func createFilesOnDisk(t *testing.T, fs afero.Fs, basePath string, files map[string][]byte) {
	t.Helper()

	for path, content := range files {
		fullPath := basePath + "/" + path
		require.NoError(t, fs.MkdirAll(basePath, 0o755))
		require.NoError(t, afero.WriteFile(fs, fullPath, content, 0o644))
	}
}

func TestNewChecker(t *testing.T) {
	t.Run("valid manifest", func(t *testing.T) {
		fs := afero.NewMemMapFs()
		files := map[string][]byte{
			"file1.txt": []byte("hello"),
			"file2.txt": []byte("world"),
		}
		createTestManifest(t, fs, "/manifest.mf", files)

		chk, err := NewChecker("/manifest.mf", "/", fs)
		require.NoError(t, err)
		assert.NotNil(t, chk)
		assert.Equal(t, FileCount(2), chk.FileCount())
	})

	t.Run("missing manifest", func(t *testing.T) {
		fs := afero.NewMemMapFs()
		_, err := NewChecker("/nonexistent.mf", "/", fs)
		assert.Error(t, err)
	})

	t.Run("invalid manifest", func(t *testing.T) {
		fs := afero.NewMemMapFs()
		require.NoError(t, afero.WriteFile(fs, "/bad.mf", []byte("not a manifest"), 0o644))
		_, err := NewChecker("/bad.mf", "/", fs)
		assert.Error(t, err)
	})
}

func TestCheckerFileCountAndTotalBytes(t *testing.T) {
	fs := afero.NewMemMapFs()
	files := map[string][]byte{
		"small.txt":  []byte("hi"),
		"medium.txt": []byte("hello world"),
		"large.txt":  bytes.Repeat([]byte("x"), 1000),
	}
	createTestManifest(t, fs, "/manifest.mf", files)

	chk, err := NewChecker("/manifest.mf", "/", fs)
	require.NoError(t, err)

	assert.Equal(t, FileCount(3), chk.FileCount())
	assert.Equal(t, FileSize(2+11+1000), chk.TotalBytes())
}

func TestCheckAllFilesOK(t *testing.T) {
	fs := afero.NewMemMapFs()
	files := map[string][]byte{
		"file1.txt": []byte("content one"),
		"file2.txt": []byte("content two"),
	}
	createTestManifest(t, fs, "/manifest.mf", files)
	createFilesOnDisk(t, fs, "/data", files)

	chk, err := NewChecker("/manifest.mf", "/data", fs)
	require.NoError(t, err)

	results := make(chan Result, 10)
	err = chk.Check(context.Background(), results, nil)
	require.NoError(t, err)

	var resultList []Result
	for r := range results {
		resultList = append(resultList, r)
	}

	assert.Len(t, resultList, 2)
	for _, r := range resultList {
		assert.Equal(t, StatusOK, r.Status, "file %s should be OK", r.Path)
	}
}

func TestCheckMissingFile(t *testing.T) {
	fs := afero.NewMemMapFs()
	files := map[string][]byte{
		"exists.txt":  []byte("I exist"),
		"missing.txt": []byte("I don't exist on disk"),
	}
	createTestManifest(t, fs, "/manifest.mf", files)
	// Only create one file
	createFilesOnDisk(t, fs, "/data", map[string][]byte{
		"exists.txt": []byte("I exist"),
	})

	chk, err := NewChecker("/manifest.mf", "/data", fs)
	require.NoError(t, err)

	results := make(chan Result, 10)
	err = chk.Check(context.Background(), results, nil)
	require.NoError(t, err)

	var okCount, missingCount int
	for r := range results {
		switch r.Status {
		case StatusOK:
			okCount++
		case StatusMissing:
			missingCount++
			assert.Equal(t, RelFilePath("missing.txt"), r.Path)
		}
	}

	assert.Equal(t, 1, okCount)
	assert.Equal(t, 1, missingCount)
}

func TestCheckSizeMismatch(t *testing.T) {
	fs := afero.NewMemMapFs()
	files := map[string][]byte{
		"file.txt": []byte("original content"),
	}
	createTestManifest(t, fs, "/manifest.mf", files)
	// Create file with different size
	createFilesOnDisk(t, fs, "/data", map[string][]byte{
		"file.txt": []byte("short"),
	})

	chk, err := NewChecker("/manifest.mf", "/data", fs)
	require.NoError(t, err)

	results := make(chan Result, 10)
	err = chk.Check(context.Background(), results, nil)
	require.NoError(t, err)

	r := <-results
	assert.Equal(t, StatusSizeMismatch, r.Status)
	assert.Equal(t, RelFilePath("file.txt"), r.Path)
}

func TestCheckHashMismatch(t *testing.T) {
	fs := afero.NewMemMapFs()
	originalContent := []byte("original content")
	files := map[string][]byte{
		"file.txt": originalContent,
	}
	createTestManifest(t, fs, "/manifest.mf", files)
	// Create file with same size but different content
	differentContent := []byte("different contnt") // same length (16 bytes) but different
	require.Equal(t, len(originalContent), len(differentContent), "test requires same length")
	createFilesOnDisk(t, fs, "/data", map[string][]byte{
		"file.txt": differentContent,
	})

	chk, err := NewChecker("/manifest.mf", "/data", fs)
	require.NoError(t, err)

	results := make(chan Result, 10)
	err = chk.Check(context.Background(), results, nil)
	require.NoError(t, err)

	r := <-results
	assert.Equal(t, StatusHashMismatch, r.Status)
	assert.Equal(t, RelFilePath("file.txt"), r.Path)
}

func TestCheckWithProgress(t *testing.T) {
	fs := afero.NewMemMapFs()
	files := map[string][]byte{
		"file1.txt": bytes.Repeat([]byte("a"), 100),
		"file2.txt": bytes.Repeat([]byte("b"), 200),
	}
	createTestManifest(t, fs, "/manifest.mf", files)
	createFilesOnDisk(t, fs, "/data", files)

	chk, err := NewChecker("/manifest.mf", "/data", fs)
	require.NoError(t, err)

	results := make(chan Result, 10)
	progress := make(chan CheckStatus, 10)

	err = chk.Check(context.Background(), results, progress)
	require.NoError(t, err)

	// Drain results
	for range results {
	}

	// Check progress was sent
	var progressUpdates []CheckStatus
	for p := range progress {
		progressUpdates = append(progressUpdates, p)
	}

	assert.NotEmpty(t, progressUpdates)
	// Final progress should show all files checked
	final := progressUpdates[len(progressUpdates)-1]
	assert.Equal(t, FileCount(2), final.TotalFiles)
	assert.Equal(t, FileCount(2), final.CheckedFiles)
	assert.Equal(t, FileSize(300), final.TotalBytes)
	assert.Equal(t, FileSize(300), final.CheckedBytes)
	assert.Equal(t, FileCount(0), final.Failures)
}

func TestCheckContextCancellation(t *testing.T) {
	fs := afero.NewMemMapFs()
	// Create many files to ensure we have time to cancel
	files := make(map[string][]byte)
	for i := 0; i < 100; i++ {
		files[string(rune('a'+i%26))+".txt"] = bytes.Repeat([]byte("x"), 1000)
	}
	createTestManifest(t, fs, "/manifest.mf", files)
	createFilesOnDisk(t, fs, "/data", files)

	chk, err := NewChecker("/manifest.mf", "/data", fs)
	require.NoError(t, err)

	ctx, cancel := context.WithCancel(context.Background())
	cancel() // Cancel immediately

	results := make(chan Result, 200)
	err = chk.Check(ctx, results, nil)
	assert.ErrorIs(t, err, context.Canceled)
}

func TestFindExtraFiles(t *testing.T) {
	fs := afero.NewMemMapFs()
	// Manifest only contains file1
	manifestFiles := map[string][]byte{
		"file1.txt": []byte("in manifest"),
	}
	createTestManifest(t, fs, "/manifest.mf", manifestFiles)

	// Disk has file1 and file2
	createFilesOnDisk(t, fs, "/data", map[string][]byte{
		"file1.txt": []byte("in manifest"),
		"file2.txt": []byte("extra file"),
	})

	chk, err := NewChecker("/manifest.mf", "/data", fs)
	require.NoError(t, err)

	results := make(chan Result, 10)
	err = chk.FindExtraFiles(context.Background(), results)
	require.NoError(t, err)

	var extras []Result
	for r := range results {
		extras = append(extras, r)
	}

	assert.Len(t, extras, 1)
	assert.Equal(t, RelFilePath("file2.txt"), extras[0].Path)
	assert.Equal(t, StatusExtra, extras[0].Status)
	assert.Equal(t, "not in manifest", extras[0].Message)
}

func TestFindExtraFilesContextCancellation(t *testing.T) {
	fs := afero.NewMemMapFs()
	files := map[string][]byte{"file.txt": []byte("data")}
	createTestManifest(t, fs, "/manifest.mf", files)
	createFilesOnDisk(t, fs, "/data", files)

	chk, err := NewChecker("/manifest.mf", "/data", fs)
	require.NoError(t, err)

	ctx, cancel := context.WithCancel(context.Background())
	cancel() // Cancel immediately

	results := make(chan Result, 10)
	err = chk.FindExtraFiles(ctx, results)
	assert.ErrorIs(t, err, context.Canceled)
}

func TestCheckNilChannels(t *testing.T) {
	fs := afero.NewMemMapFs()
	files := map[string][]byte{"file.txt": []byte("data")}
	createTestManifest(t, fs, "/manifest.mf", files)
	createFilesOnDisk(t, fs, "/data", files)

	chk, err := NewChecker("/manifest.mf", "/data", fs)
	require.NoError(t, err)

	// Should not panic with nil channels
	err = chk.Check(context.Background(), nil, nil)
	assert.NoError(t, err)
}

func TestFindExtraFilesNilChannel(t *testing.T) {
	fs := afero.NewMemMapFs()
	files := map[string][]byte{"file.txt": []byte("data")}
	createTestManifest(t, fs, "/manifest.mf", files)
	createFilesOnDisk(t, fs, "/data", files)

	chk, err := NewChecker("/manifest.mf", "/data", fs)
	require.NoError(t, err)

	// Should not panic with nil channel
	err = chk.FindExtraFiles(context.Background(), nil)
	assert.NoError(t, err)
}

func TestCheckSubdirectories(t *testing.T) {
	fs := afero.NewMemMapFs()
	files := map[string][]byte{
		"dir1/file1.txt":          []byte("content1"),
		"dir1/dir2/file2.txt":     []byte("content2"),
		"dir1/dir2/dir3/deep.txt": []byte("deep content"),
	}
	createTestManifest(t, fs, "/manifest.mf", files)

	// Create files with full directory structure
	for path, content := range files {
		fullPath := "/data/" + path
		require.NoError(t, fs.MkdirAll("/data/dir1/dir2/dir3", 0o755))
		require.NoError(t, afero.WriteFile(fs, fullPath, content, 0o644))
	}

	chk, err := NewChecker("/manifest.mf", "/data", fs)
	require.NoError(t, err)

	results := make(chan Result, 10)
	err = chk.Check(context.Background(), results, nil)
	require.NoError(t, err)

	var okCount int
	for r := range results {
		assert.Equal(t, StatusOK, r.Status, "file %s should be OK", r.Path)
		okCount++
	}
	assert.Equal(t, 3, okCount)
}

func TestCheckEmptyManifest(t *testing.T) {
	fs := afero.NewMemMapFs()
	// Create manifest with no files
	createTestManifest(t, fs, "/manifest.mf", map[string][]byte{})

	chk, err := NewChecker("/manifest.mf", "/data", fs)
	require.NoError(t, err)

	assert.Equal(t, FileCount(0), chk.FileCount())
	assert.Equal(t, FileSize(0), chk.TotalBytes())

	results := make(chan Result, 10)
	err = chk.Check(context.Background(), results, nil)
	require.NoError(t, err)

	var count int
	for range results {
		count++
	}
	assert.Equal(t, 0, count)
}
