//go:build linux
// +build linux

package devicemapper

import (
	"errors"
	"flag"
	"os"
	"syscall"
	"testing"
	"unsafe"

	"golang.org/x/sys/unix"
)

var (
	integration = flag.Bool("integration", false, "run integration tests")
)

func clearTestDependencies() {
	removeDeviceWrapper = removeDevice
	openMapperWrapper = openMapper
}

func TestMain(m *testing.M) {
	flag.Parse()
	m.Run()
}

func validateDevice(t *testing.T, p string, sectors int64, writable bool) {
	t.Helper()
	dev, err := os.OpenFile(p, os.O_RDWR|os.O_SYNC, 0)
	if err != nil {
		t.Fatal(err)
	}
	defer dev.Close()

	var size int64
	_, _, errno := unix.Syscall(unix.SYS_IOCTL, dev.Fd(), unix.BLKGETSIZE64, uintptr(unsafe.Pointer(&size)))
	if errno != 0 {
		t.Fatal(errno)
	}
	if size != sectors*512 {
		t.Fatalf("expected %d bytes, got %d", sectors*512, size)
	}

	var b [512]byte
	_, err = unix.Read(int(dev.Fd()), b[:])
	if !errors.Is(err, unix.EIO) {
		t.Fatalf("expected EIO, got %s", err)
	}
	_, err = unix.Write(int(dev.Fd()), b[:])
	if writable && !errors.Is(err, unix.EIO) {
		t.Fatalf("expected EIO, got %s", err)
	} else if !errors.Is(err, unix.EPERM) {
		t.Fatalf("expected EPERM, got %s", err)
	}
}

type device struct {
	Name, Path string
}

func (d *device) Close() (err error) {
	if d.Name != "" {
		err = RemoveDevice(d.Name)
		if err == nil {
			d.Name = ""
		}
	}
	return err
}

func createDevice(name string, flags CreateFlags, targets []Target) (*device, error) {
	p, err := CreateDevice(name, flags, targets)
	if err != nil {
		return nil, err
	}
	return &device{Name: name, Path: p}, nil
}

func TestCreateError(t *testing.T) {
	clearTestDependencies()

	if !*integration {
		t.Skip()
	}
	d, err := createDevice("test-device", 0, []Target{
		{Type: "error", SectorStart: 0, LengthInBlocks: 1},
		{Type: "error", SectorStart: 1, LengthInBlocks: 2},
	})
	if err != nil {
		t.Fatal(err)
	}
	defer d.Close()
	validateDevice(t, d.Path, 3, true)
	err = d.Close()
	if err != nil {
		t.Fatal(err)
	}
}

func TestReadOnlyError(t *testing.T) {
	clearTestDependencies()

	if !*integration {
		t.Skip()
	}
	d, err := createDevice("test-device", CreateReadOnly, []Target{
		{Type: "error", SectorStart: 0, LengthInBlocks: 1},
		{Type: "error", SectorStart: 1, LengthInBlocks: 2},
	})
	if err != nil {
		t.Fatal(err)
	}
	defer d.Close()
	validateDevice(t, d.Path, 3, false)
	err = d.Close()
	if err != nil {
		t.Fatal(err)
	}
}

func TestLinearError(t *testing.T) {
	clearTestDependencies()

	if !*integration {
		t.Skip()
	}
	b, err := createDevice("base-device", 0, []Target{
		{Type: "error", SectorStart: 0, LengthInBlocks: 100},
	})
	if err != nil {
		t.Fatal(err)
	}
	defer b.Close()
	d, err := createDevice("linear-device", 0, []Target{
		LinearTarget(0, 50, b.Path, 50),
	})
	if err != nil {
		t.Fatal(err)
	}
	defer d.Close()
	validateDevice(t, d.Path, 50, true)
	err = d.Close()
	if err != nil {
		t.Fatal(err)
	}
	err = b.Close()
	if err != nil {
		t.Fatal(err)
	}
}

func TestRemoveDeviceRetriesOnSyscallEBUSY(t *testing.T) {
	clearTestDependencies()

	rmDeviceCalled := false
	retryDone := false
	// Overrides openMapper to return temp file handle
	openMapperWrapper = func() (*os.File, error) {
		return os.CreateTemp("", "")
	}
	removeDeviceWrapper = func(_ *os.File, _ string) error {
		if !rmDeviceCalled {
			rmDeviceCalled = true
			return &dmError{
				Op:  1,
				Err: syscall.EBUSY,
			}
		}
		if !retryDone {
			retryDone = true
			return nil
		}
		return nil
	}

	if err := RemoveDevice("test"); err != nil {
		t.Fatalf("expected no error, got: %s", err)
	}
	if !rmDeviceCalled {
		t.Fatalf("expected removeDevice to be called at least once")
	}
	if !retryDone {
		t.Fatalf("expected removeDevice to be retried after initial failure")
	}
}

func TestRemoveDeviceFailsOnNonSyscallEBUSY(t *testing.T) {
	clearTestDependencies()

	expectedError := &dmError{
		Op:  0,
		Err: syscall.EACCES,
	}
	rmDeviceCalled := false
	retryDone := false
	openMapperWrapper = func() (*os.File, error) {
		return os.CreateTemp("", "")
	}
	removeDeviceWrapper = func(_ *os.File, _ string) error {
		if !rmDeviceCalled {
			rmDeviceCalled = true
			return expectedError
		}
		if !retryDone {
			retryDone = true
			return nil
		}
		return nil
	}

	if err := RemoveDevice("test"); err != expectedError { //nolint:errorlint
		t.Fatalf("expected error %q, instead got %q", expectedError, err)
	}
	if !rmDeviceCalled {
		t.Fatalf("expected removeDevice to be called once")
	}
	if retryDone {
		t.Fatalf("no retries should've been attempted")
	}
}
