package content

import (
	"os"
	"path/filepath"
	"runtime"
	"sync/atomic"
	"testing"

	"github.com/pkg/errors"
	"github.com/stretchr/testify/require"
)

// TestWriteTempFileAtomic_HappyPath verifies that writeTempFileAtomic writes
// the expected content and returns a valid file path.
func TestWriteTempFileAtomic_HappyPath(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	data := []byte("index-blob-content")

	name, err := writeTempFileAtomicImp(localFS{}, dir, data)
	require.NoError(t, err)
	require.NotEmpty(t, name)

	// File must exist under the given directory.
	require.Equal(t, dir, filepath.Dir(name))

	got, err := os.ReadFile(name)
	require.NoError(t, err)
	require.Equal(t, data, got)
}

// TestWriteTempFileAtomic_EmptyData verifies that an empty payload is written
// without error and produces a valid (zero-byte) file.
func TestWriteTempFileAtomic_EmptyData(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()

	name, err := writeTempFileAtomicImp(localFS{}, dir, []byte{})
	require.NoError(t, err)

	info, err := os.Stat(name)
	require.NoError(t, err)
	require.EqualValues(t, 0, info.Size())
}

// TestWriteTempFileAtomic_CreatesDirectoryIfMissing verifies that
// writeTempFileAtomic creates the target directory when it does not exist,
// matching the MkdirAll fallback path.
func TestWriteTempFileAtomic_CreatesDirectoryIfMissing(t *testing.T) {
	t.Parallel()

	// Use a path that does not yet exist.
	dir := filepath.Join(t.TempDir(), "new", "nested", "dir")

	data := []byte("hello")

	name, err := writeTempFileAtomicImp(localFS{}, dir, data)
	require.NoError(t, err)
	require.Equal(t, dir, filepath.Dir(name))

	got, err := os.ReadFile(name)
	require.NoError(t, err)
	require.Equal(t, data, got)
}

// TestWriteTempFileAtomic_NonExistentDirUnwritable verifies that an error is
// returned when the directory cannot be created (e.g. parent is read-only).
// Skipped on platforms where root may bypass permissions.
func TestWriteTempFileAtomic_NonExistentDirUnwritable(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("does not work on windows due to chmod")
	}

	if os.Getuid() == 0 {
		t.Skip("skipping permission test when running as root")
	}

	t.Parallel()

	// Create a read-only parent so that MkdirAll cannot create the child.
	parent := t.TempDir()
	require.NoError(t, os.Chmod(parent, 0o555))

	t.Cleanup(func() { os.Chmod(parent, 0o755) }) //nolint:errcheck

	dir := filepath.Join(parent, "child")

	_, err := writeTempFileAtomicImp(localFS{}, dir, []byte("data"))
	require.Error(t, err)
	require.Contains(t, err.Error(), "cannot create parent directory for temp file")
}

type mockFileSynced struct {
	file

	synced atomic.Bool
}

func (mf *mockFileSynced) Write(p []byte) (n int, err error) {
	mf.synced.Store(false)

	return mf.file.Write(p)
}

func (mf *mockFileSynced) Sync() error {
	err := mf.file.Sync()
	if err == nil {
		mf.synced.Store(true)
	}

	return err
}

// TestWriteTempFileAtomic_FileIsSynced verifies that Sync is called after
// writing data to the temporary file.
func TestWriteTempFileAtomic_FileIsSynced(t *testing.T) {
	t.Parallel()

	var mockedFile mockFileSynced

	dir := t.TempDir()
	data := []byte("synced-content")

	mfs := mockfs{
		createWrapper: func(f file) file {
			mockedFile.file = f

			return &mockedFile
		},
	}

	name, err := writeTempFileAtomicImp(mfs, dir, data)
	require.NoError(t, err)
	require.True(t, mockedFile.synced.Load())

	// Open a new handle to avoid OS read-cache of the same descriptor.
	b, err := os.ReadFile(name)
	require.NoError(t, err)
	require.Equal(t, data, b)
}

// TestWriteTempFileAtomic_NoTempFilesLeft verifies that writeTempFileAtomic
// does not leak the temporary file after a successful call — the caller is
// expected to rename it, but the file descriptor must already be closed.
// We confirm this indirectly: the returned path must be stat-able (file
// exists and is closed) with no other tmp* siblings beyond the returned one.
func TestWriteTempFileAtomic_NoTempFilesLeft(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()

	name, err := writeTempFileAtomicImp(localFS{}, dir, []byte("data"))
	require.NoError(t, err)

	entries, err := os.ReadDir(dir)
	require.NoError(t, err)

	// Only one file should exist: the one returned.
	require.Len(t, entries, 1)
	require.Equal(t, filepath.Base(name), entries[0].Name())
}

type mockfs struct {
	localFS

	createWrapper func(file) file
}

func (m mockfs) CreateTemp(dir, pattern string) (file, error) {
	f, err := m.localFS.CreateTemp(dir, pattern)

	if m.createWrapper != nil {
		f = m.createWrapper(f)
	}

	return f, err
}

type mockFileWriteError struct {
	file
}

func (mf mockFileWriteError) Write(p []byte) (n int, err error) {
	return 0, errors.New("mock file write error")
}

type mockFileSyncError struct {
	file
}

func (mf mockFileSyncError) Sync() error {
	return errors.New("mock file sync error")
}

type mockFileCloseError struct {
	file
}

func (mf mockFileCloseError) Close() error {
	if err := mf.file.Close(); err != nil {
		return err
	}

	return errors.New("mock file close error")
}

// TestWriteTempFileAtomic_NoTempFilesLeftOnError verifies that writeTempFileAtomic
// does not leak the temporary file after a write, sync, or close error.
func TestWriteTempFileAtomic_NoTempFilesLeftOnError(t *testing.T) {
	t.Parallel()

	cases := []struct {
		mockfs
		description string
	}{
		{
			description: "write-error",
			mockfs: mockfs{
				createWrapper: func(f file) file { return mockFileWriteError{file: f} },
			},
		},
		{
			description: "sync-error",
			mockfs: mockfs{
				createWrapper: func(f file) file { return mockFileSyncError{file: f} },
			},
		},
		{
			description: "close-error",
			mockfs: mockfs{
				createWrapper: func(f file) file { return mockFileCloseError{file: f} },
			},
		},
	}

	for _, c := range cases {
		t.Run(c.description, func(t *testing.T) {
			dir := t.TempDir()

			name, err := writeTempFileAtomicImp(c.mockfs, dir, []byte("data"))
			require.Error(t, err)
			require.Empty(t, name)
			t.Log("error:", err)

			entries, err := os.ReadDir(dir)
			require.NoError(t, err)
			require.Empty(t, entries)
		})
	}
}
