package mfer

import (
	"bytes"
	"strings"
	"testing"
	"time"

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

func TestNewBuilder(t *testing.T) {
	b := NewBuilder()
	assert.NotNil(t, b)
	assert.Equal(t, 0, b.FileCount())
}

func TestBuilderAddFile(t *testing.T) {
	b := NewBuilder()
	content := []byte("test content")
	reader := bytes.NewReader(content)

	bytesRead, err := b.AddFile("test.txt", FileSize(len(content)), ModTime(time.Now()), reader, nil)
	require.NoError(t, err)
	assert.Equal(t, FileSize(len(content)), bytesRead)
	assert.Equal(t, 1, b.FileCount())
}

func TestBuilderAddFileWithHash(t *testing.T) {
	b := NewBuilder()
	hash := make([]byte, 34) // SHA256 multihash is 34 bytes

	err := b.AddFileWithHash("test.txt", 100, ModTime(time.Now()), hash)
	require.NoError(t, err)
	assert.Equal(t, 1, b.FileCount())
}

func TestBuilderAddFileWithHashValidation(t *testing.T) {
	t.Run("empty path", func(t *testing.T) {
		b := NewBuilder()
		hash := make([]byte, 34)
		err := b.AddFileWithHash("", 100, ModTime(time.Now()), hash)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "path")
	})

	t.Run("negative size", func(t *testing.T) {
		b := NewBuilder()
		hash := make([]byte, 34)
		err := b.AddFileWithHash("test.txt", -1, ModTime(time.Now()), hash)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "size")
	})

	t.Run("nil hash", func(t *testing.T) {
		b := NewBuilder()
		err := b.AddFileWithHash("test.txt", 100, ModTime(time.Now()), nil)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "hash")
	})

	t.Run("empty hash", func(t *testing.T) {
		b := NewBuilder()
		err := b.AddFileWithHash("test.txt", 100, ModTime(time.Now()), []byte{})
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "hash")
	})

	t.Run("valid inputs", func(t *testing.T) {
		b := NewBuilder()
		hash := make([]byte, 34)
		err := b.AddFileWithHash("test.txt", 100, ModTime(time.Now()), hash)
		assert.NoError(t, err)
		assert.Equal(t, 1, b.FileCount())
	})
}

func TestBuilderBuild(t *testing.T) {
	b := NewBuilder()
	content := []byte("test content")
	reader := bytes.NewReader(content)

	_, err := b.AddFile("test.txt", FileSize(len(content)), ModTime(time.Now()), reader, nil)
	require.NoError(t, err)

	var buf bytes.Buffer
	err = b.Build(&buf)
	require.NoError(t, err)

	// Should have magic bytes
	assert.True(t, strings.HasPrefix(buf.String(), MAGIC))
}

func TestNewTimestampFromTimeExtremeDate(t *testing.T) {
	// Regression test: newTimestampFromTime used UnixNano() which panics
	// for dates outside ~1678-2262. Now uses Nanosecond() which is safe.
	tests := []struct {
		name string
		time time.Time
	}{
		{"zero time", time.Time{}},
		{"year 1000", time.Date(1000, 1, 1, 0, 0, 0, 0, time.UTC)},
		{"year 3000", time.Date(3000, 1, 1, 0, 0, 0, 123456789, time.UTC)},
		{"unix epoch", time.Unix(0, 0)},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Should not panic
			ts := newTimestampFromTime(tt.time)
			assert.Equal(t, tt.time.Unix(), ts.Seconds)
			assert.Equal(t, int32(tt.time.Nanosecond()), ts.Nanos)
		})
	}
}

func TestBuilderBuildDeterministicOrder(t *testing.T) {
	// Regression test for #23: files should be sorted by path in the manifest
	// to ensure deterministic output regardless of insertion order.
	buildManifest := func(paths []string) []byte {
		b := NewBuilder()
		for _, p := range paths {
			content := []byte("content of " + p)
			reader := bytes.NewReader(content)
			_, err := b.AddFile(RelFilePath(p), FileSize(len(content)), ModTime(time.Now()), reader, nil)
			require.NoError(t, err)
		}
		var buf bytes.Buffer
		require.NoError(t, b.Build(&buf))
		return buf.Bytes()
	}

	// Build with files in two different orders
	order1 := []string{"z.txt", "a.txt", "m/b.txt", "m/a.txt", "b.txt"}
	order2 := []string{"b.txt", "m/a.txt", "a.txt", "z.txt", "m/b.txt"}

	manifest1 := buildManifest(order1)
	manifest2 := buildManifest(order2)

	// Parse both and verify file order is sorted
	m1, err := NewManifestFromReader(bytes.NewReader(manifest1))
	require.NoError(t, err)
	m2, err := NewManifestFromReader(bytes.NewReader(manifest2))
	require.NoError(t, err)

	files1 := m1.Files()
	files2 := m2.Files()
	require.Len(t, files1, 5)
	require.Len(t, files2, 5)

	// Both should have same order
	for i := range files1 {
		assert.Equal(t, files1[i].Path, files2[i].Path, "file %d path mismatch", i)
	}

	// Verify the order is lexicographic
	assert.Equal(t, "a.txt", files1[0].Path)
	assert.Equal(t, "b.txt", files1[1].Path)
	assert.Equal(t, "m/a.txt", files1[2].Path)
	assert.Equal(t, "m/b.txt", files1[3].Path)
	assert.Equal(t, "z.txt", files1[4].Path)
}

func TestValidatePath(t *testing.T) {
	valid := []string{
		"file.txt",
		"dir/file.txt",
		"a/b/c/d.txt",
		"file with spaces.txt",
		"日本語.txt",
	}
	for _, p := range valid {
		t.Run("valid:"+p, func(t *testing.T) {
			assert.NoError(t, ValidatePath(p))
		})
	}

	invalid := []struct {
		path string
		desc string
	}{
		{"", "empty"},
		{"/absolute", "absolute path"},
		{"has\\backslash", "backslash"},
		{"has/../traversal", "dot-dot segment"},
		{"has//double", "empty segment"},
		{"..", "just dot-dot"},
		{string([]byte{0xff, 0xfe}), "invalid UTF-8"},
	}
	for _, tt := range invalid {
		t.Run("invalid:"+tt.desc, func(t *testing.T) {
			assert.Error(t, ValidatePath(tt.path))
		})
	}
}

func TestBuilderAddFileSizeMismatch(t *testing.T) {
	b := NewBuilder()
	content := []byte("short")
	reader := bytes.NewReader(content)

	// Declare wrong size
	_, err := b.AddFile("test.txt", FileSize(100), ModTime(time.Now()), reader, nil)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "size mismatch")
}

func TestBuilderAddFileInvalidPath(t *testing.T) {
	b := NewBuilder()
	content := []byte("data")
	reader := bytes.NewReader(content)

	_, err := b.AddFile("", FileSize(len(content)), ModTime(time.Now()), reader, nil)
	assert.Error(t, err)

	reader.Reset(content)
	_, err = b.AddFile("/absolute", FileSize(len(content)), ModTime(time.Now()), reader, nil)
	assert.Error(t, err)
}

func TestBuilderAddFileWithProgress(t *testing.T) {
	b := NewBuilder()
	content := bytes.Repeat([]byte("x"), 1000)
	reader := bytes.NewReader(content)
	progress := make(chan FileHashProgress, 100)

	bytesRead, err := b.AddFile("test.txt", FileSize(len(content)), ModTime(time.Now()), reader, progress)
	close(progress)
	require.NoError(t, err)
	assert.Equal(t, FileSize(1000), bytesRead)

	var updates []FileHashProgress
	for p := range progress {
		updates = append(updates, p)
	}
	assert.NotEmpty(t, updates)
	// Last update should show all bytes
	assert.Equal(t, FileSize(1000), updates[len(updates)-1].BytesRead)
}

func TestBuilderBuildRoundTrip(t *testing.T) {
	// Build a manifest, deserialize it, verify all fields survive round-trip
	b := NewBuilder()
	now := time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC)

	files := []struct {
		path    string
		content []byte
	}{
		{"alpha.txt", []byte("alpha content")},
		{"beta/gamma.txt", []byte("gamma content")},
		{"beta/delta.txt", []byte("delta content")},
	}

	for _, f := range files {
		reader := bytes.NewReader(f.content)
		_, err := b.AddFile(RelFilePath(f.path), FileSize(len(f.content)), ModTime(now), reader, nil)
		require.NoError(t, err)
	}

	var buf bytes.Buffer
	require.NoError(t, b.Build(&buf))

	m, err := NewManifestFromReader(&buf)
	require.NoError(t, err)

	mfiles := m.Files()
	require.Len(t, mfiles, 3)

	// Verify sorted order
	assert.Equal(t, "alpha.txt", mfiles[0].Path)
	assert.Equal(t, "beta/delta.txt", mfiles[1].Path)
	assert.Equal(t, "beta/gamma.txt", mfiles[2].Path)

	// Verify sizes
	assert.Equal(t, int64(len("alpha content")), mfiles[0].Size)

	// Verify hashes are present
	for _, f := range mfiles {
		require.NotEmpty(t, f.Hashes, "file %s should have hashes", f.Path)
		assert.NotEmpty(t, f.Hashes[0].MultiHash)
	}
}

func TestNewManifestFromReaderInvalidMagic(t *testing.T) {
	_, err := NewManifestFromReader(bytes.NewReader([]byte("NOT_VALID")))
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "invalid file format")
}

func TestNewManifestFromReaderEmpty(t *testing.T) {
	_, err := NewManifestFromReader(bytes.NewReader([]byte{}))
	assert.Error(t, err)
}

func TestNewManifestFromReaderTruncated(t *testing.T) {
	// Just the magic with nothing after
	_, err := NewManifestFromReader(bytes.NewReader([]byte(MAGIC)))
	assert.Error(t, err)
}

func TestManifestString(t *testing.T) {
	b := NewBuilder()
	content := []byte("test")
	reader := bytes.NewReader(content)
	_, err := b.AddFile("test.txt", FileSize(len(content)), ModTime(time.Now()), reader, nil)
	require.NoError(t, err)

	var buf bytes.Buffer
	require.NoError(t, b.Build(&buf))

	m, err := NewManifestFromReader(&buf)
	require.NoError(t, err)
	assert.Contains(t, m.String(), "count=1")
}

func TestBuilderBuildEmpty(t *testing.T) {
	b := NewBuilder()

	var buf bytes.Buffer
	err := b.Build(&buf)
	require.NoError(t, err)

	// Should still produce valid manifest with 0 files
	assert.True(t, strings.HasPrefix(buf.String(), MAGIC))
}
