// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package os_test

import (
	"bytes"
	"internal/poll"
	"io"
	"math/rand"
	"os"
	. "os"
	"path/filepath"
	"strconv"
	"syscall"
	"testing"
	"time"
)

func TestCopyFileRange(t *testing.T) {
	sizes := []int{
		1,
		42,
		1025,
		syscall.Getpagesize() + 1,
		32769,
	}
	t.Run("Basic", func(t *testing.T) {
		for _, size := range sizes {
			t.Run(strconv.Itoa(size), func(t *testing.T) {
				testCopyFileRange(t, int64(size), -1)
			})
		}
	})
	t.Run("Limited", func(t *testing.T) {
		t.Run("OneLess", func(t *testing.T) {
			for _, size := range sizes {
				t.Run(strconv.Itoa(size), func(t *testing.T) {
					testCopyFileRange(t, int64(size), int64(size)-1)
				})
			}
		})
		t.Run("Half", func(t *testing.T) {
			for _, size := range sizes {
				t.Run(strconv.Itoa(size), func(t *testing.T) {
					testCopyFileRange(t, int64(size), int64(size)/2)
				})
			}
		})
		t.Run("More", func(t *testing.T) {
			for _, size := range sizes {
				t.Run(strconv.Itoa(size), func(t *testing.T) {
					testCopyFileRange(t, int64(size), int64(size)+7)
				})
			}
		})
	})
	t.Run("DoesntTryInAppendMode", func(t *testing.T) {
		dst, src, data, hook := newCopyFileRangeTest(t, 42)

		dst2, err := OpenFile(dst.Name(), O_RDWR|O_APPEND, 0755)
		if err != nil {
			t.Fatal(err)
		}
		defer dst2.Close()

		if _, err := io.Copy(dst2, src); err != nil {
			t.Fatal(err)
		}
		if hook.called {
			t.Fatal("called poll.CopyFileRange for destination in O_APPEND mode")
		}
		mustSeekStart(t, dst2)
		mustContainData(t, dst2, data) // through traditional means
	})
	t.Run("NotRegular", func(t *testing.T) {
		t.Run("BothPipes", func(t *testing.T) {
			hook := hookCopyFileRange(t)

			pr1, pw1, err := Pipe()
			if err != nil {
				t.Fatal(err)
			}
			defer pr1.Close()
			defer pw1.Close()

			pr2, pw2, err := Pipe()
			if err != nil {
				t.Fatal(err)
			}
			defer pr2.Close()
			defer pw2.Close()

			// The pipe is empty, and PIPE_BUF is large enough
			// for this, by (POSIX) definition, so there is no
			// need for an additional goroutine.
			data := []byte("hello")
			if _, err := pw1.Write(data); err != nil {
				t.Fatal(err)
			}
			pw1.Close()

			n, err := io.Copy(pw2, pr1)
			if err != nil {
				t.Fatal(err)
			}
			if n != int64(len(data)) {
				t.Fatalf("transferred %d, want %d", n, len(data))
			}
			if !hook.called {
				t.Fatalf("should have called poll.CopyFileRange")
			}
			pw2.Close()
			mustContainData(t, pr2, data)
		})
		t.Run("DstPipe", func(t *testing.T) {
			dst, src, data, hook := newCopyFileRangeTest(t, 255)
			dst.Close()

			pr, pw, err := Pipe()
			if err != nil {
				t.Fatal(err)
			}
			defer pr.Close()
			defer pw.Close()

			n, err := io.Copy(pw, src)
			if err != nil {
				t.Fatal(err)
			}
			if n != int64(len(data)) {
				t.Fatalf("transferred %d, want %d", n, len(data))
			}
			if !hook.called {
				t.Fatalf("should have called poll.CopyFileRange")
			}
			pw.Close()
			mustContainData(t, pr, data)
		})
		t.Run("SrcPipe", func(t *testing.T) {
			dst, src, data, hook := newCopyFileRangeTest(t, 255)
			src.Close()

			pr, pw, err := Pipe()
			if err != nil {
				t.Fatal(err)
			}
			defer pr.Close()
			defer pw.Close()

			// The pipe is empty, and PIPE_BUF is large enough
			// for this, by (POSIX) definition, so there is no
			// need for an additional goroutine.
			if _, err := pw.Write(data); err != nil {
				t.Fatal(err)
			}
			pw.Close()

			n, err := io.Copy(dst, pr)
			if err != nil {
				t.Fatal(err)
			}
			if n != int64(len(data)) {
				t.Fatalf("transferred %d, want %d", n, len(data))
			}
			if !hook.called {
				t.Fatalf("should have called poll.CopyFileRange")
			}
			mustSeekStart(t, dst)
			mustContainData(t, dst, data)
		})
	})
	t.Run("Nil", func(t *testing.T) {
		var nilFile *File
		anyFile, err := os.CreateTemp("", "")
		if err != nil {
			t.Fatal(err)
		}
		defer Remove(anyFile.Name())
		defer anyFile.Close()

		if _, err := io.Copy(nilFile, nilFile); err != ErrInvalid {
			t.Errorf("io.Copy(nilFile, nilFile) = %v, want %v", err, ErrInvalid)
		}
		if _, err := io.Copy(anyFile, nilFile); err != ErrInvalid {
			t.Errorf("io.Copy(anyFile, nilFile) = %v, want %v", err, ErrInvalid)
		}
		if _, err := io.Copy(nilFile, anyFile); err != ErrInvalid {
			t.Errorf("io.Copy(nilFile, anyFile) = %v, want %v", err, ErrInvalid)
		}

		if _, err := nilFile.ReadFrom(nilFile); err != ErrInvalid {
			t.Errorf("nilFile.ReadFrom(nilFile) = %v, want %v", err, ErrInvalid)
		}
		if _, err := anyFile.ReadFrom(nilFile); err != ErrInvalid {
			t.Errorf("anyFile.ReadFrom(nilFile) = %v, want %v", err, ErrInvalid)
		}
		if _, err := nilFile.ReadFrom(anyFile); err != ErrInvalid {
			t.Errorf("nilFile.ReadFrom(anyFile) = %v, want %v", err, ErrInvalid)
		}
	})
}

func testCopyFileRange(t *testing.T, size int64, limit int64) {
	dst, src, data, hook := newCopyFileRangeTest(t, size)

	// If we have a limit, wrap the reader.
	var (
		realsrc io.Reader
		lr      *io.LimitedReader
	)
	if limit >= 0 {
		lr = &io.LimitedReader{N: limit, R: src}
		realsrc = lr
		if limit < int64(len(data)) {
			data = data[:limit]
		}
	} else {
		realsrc = src
	}

	// Now call ReadFrom (through io.Copy), which will hopefully call
	// poll.CopyFileRange.
	n, err := io.Copy(dst, realsrc)
	if err != nil {
		t.Fatal(err)
	}

	// If we didn't have a limit, we should have called poll.CopyFileRange
	// with the right file descriptor arguments.
	if limit > 0 && !hook.called {
		t.Fatal("never called poll.CopyFileRange")
	}
	if hook.called && hook.dstfd != int(dst.Fd()) {
		t.Fatalf("wrong destination file descriptor: got %d, want %d", hook.dstfd, dst.Fd())
	}
	if hook.called && hook.srcfd != int(src.Fd()) {
		t.Fatalf("wrong source file descriptor: got %d, want %d", hook.srcfd, src.Fd())
	}

	// Check that the offsets after the transfer make sense, that the size
	// of the transfer was reported correctly, and that the destination
	// file contains exactly the bytes we expect it to contain.
	dstoff, err := dst.Seek(0, io.SeekCurrent)
	if err != nil {
		t.Fatal(err)
	}
	srcoff, err := src.Seek(0, io.SeekCurrent)
	if err != nil {
		t.Fatal(err)
	}
	if dstoff != srcoff {
		t.Errorf("offsets differ: dstoff = %d, srcoff = %d", dstoff, srcoff)
	}
	if dstoff != int64(len(data)) {
		t.Errorf("dstoff = %d, want %d", dstoff, len(data))
	}
	if n != int64(len(data)) {
		t.Errorf("short ReadFrom: wrote %d bytes, want %d", n, len(data))
	}
	mustSeekStart(t, dst)
	mustContainData(t, dst, data)

	// If we had a limit, check that it was updated.
	if lr != nil {
		if want := limit - n; lr.N != want {
			t.Fatalf("didn't update limit correctly: got %d, want %d", lr.N, want)
		}
	}
}

// newCopyFileRangeTest initializes a new test for copy_file_range.
//
// It creates source and destination files, and populates the source file
// with random data of the specified size. It also hooks package os' call
// to poll.CopyFileRange and returns the hook so it can be inspected.
func newCopyFileRangeTest(t *testing.T, size int64) (dst, src *File, data []byte, hook *copyFileRangeHook) {
	t.Helper()

	hook = hookCopyFileRange(t)
	tmp := t.TempDir()

	src, err := Create(filepath.Join(tmp, "src"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { src.Close() })

	dst, err = Create(filepath.Join(tmp, "dst"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { dst.Close() })

	// Populate the source file with data, then rewind it, so it can be
	// consumed by copy_file_range(2).
	prng := rand.New(rand.NewSource(time.Now().Unix()))
	data = make([]byte, size)
	prng.Read(data)
	if _, err := src.Write(data); err != nil {
		t.Fatal(err)
	}
	if _, err := src.Seek(0, io.SeekStart); err != nil {
		t.Fatal(err)
	}

	return dst, src, data, hook
}

// mustContainData ensures that the specified file contains exactly the
// specified data.
func mustContainData(t *testing.T, f *File, data []byte) {
	t.Helper()

	got := make([]byte, len(data))
	if _, err := io.ReadFull(f, got); err != nil {
		t.Fatal(err)
	}
	if !bytes.Equal(got, data) {
		t.Fatalf("didn't get the same data back from %s", f.Name())
	}
	if _, err := f.Read(make([]byte, 1)); err != io.EOF {
		t.Fatalf("not at EOF")
	}
}

func mustSeekStart(t *testing.T, f *File) {
	if _, err := f.Seek(0, io.SeekStart); err != nil {
		t.Fatal(err)
	}
}

func hookCopyFileRange(t *testing.T) *copyFileRangeHook {
	h := new(copyFileRangeHook)
	h.install()
	t.Cleanup(h.uninstall)
	return h
}

type copyFileRangeHook struct {
	called bool
	dstfd  int
	srcfd  int
	remain int64

	original func(dst, src *poll.FD, remain int64) (int64, bool, error)
}

func (h *copyFileRangeHook) install() {
	h.original = *PollCopyFileRangeP
	*PollCopyFileRangeP = func(dst, src *poll.FD, remain int64) (int64, bool, error) {
		h.called = true
		h.dstfd = dst.Sysfd
		h.srcfd = src.Sysfd
		h.remain = remain
		return h.original(dst, src, remain)
	}
}

func (h *copyFileRangeHook) uninstall() {
	*PollCopyFileRangeP = h.original
}

// On some kernels copy_file_range fails on files in /proc.
func TestProcCopy(t *testing.T) {
	const cmdlineFile = "/proc/self/cmdline"
	cmdline, err := os.ReadFile(cmdlineFile)
	if err != nil {
		t.Skipf("can't read /proc file: %v", err)
	}
	in, err := os.Open(cmdlineFile)
	if err != nil {
		t.Fatal(err)
	}
	defer in.Close()
	outFile := filepath.Join(t.TempDir(), "cmdline")
	out, err := os.Create(outFile)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := io.Copy(out, in); err != nil {
		t.Fatal(err)
	}
	if err := out.Close(); err != nil {
		t.Fatal(err)
	}
	copy, err := os.ReadFile(outFile)
	if err != nil {
		t.Fatal(err)
	}
	if !bytes.Equal(cmdline, copy) {
		t.Errorf("copy of %q got %q want %q\n", cmdlineFile, copy, cmdline)
	}
}
