// Copyright (c) Contributors to the Apptainer project, established as
//   Apptainer a Series of LF Projects LLC.
//   For website terms of use, trademark policy, privacy policy and other
//   project policies see https://lfprojects.org/policies
// Copyright (c) 2019-2025, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE.md file distributed with the sources of this project regarding your
// rights to use or distribute this software.

package image

import (
	"bytes"
	"os"
	"os/exec"
	"testing"

	"github.com/apptainer/apptainer/internal/pkg/test"
)

// createVirtualBlockDevice creates a virtual block device
// in a file using the dd command. The test is skipped if
// the command is not available.
// @param[in] test handle to control the test, i.e., stop it in case of fatal error
// @param[in] path to the virtual block device to be created
func createVirtualBlockDevice(t *testing.T, path string) {
	cmdBin, err := exec.LookPath("dd")
	if err != nil {
		t.Skip("dd command not available, skipping the test")
	}

	arg := "of=" + path
	cmd := exec.Command(cmdBin, "if=/dev/zero", arg, "bs=1024", "count=10000")
	cmdErr := cmd.Run()
	if cmdErr != nil {
		os.RemoveAll(path)
		t.Fatalf("cannot create virtual block device: %s\n", cmdErr)
	}
}

// Create a new file system in an existing virtual block
// device. This function relies on mkfs.X to create the
// file system, the test is skipping if the command is
// not available.
// @param[in] test handle to control the test, i.e., stop it in case of fatal error
// @param[in] type of the file system to be created, e.g., ext3, fat.
// @param[in] path to the virtual block device.
func createFS(t *testing.T, fsType string, path string) {
	cmdBin, lookErr := exec.LookPath("mke2fs")
	if lookErr != nil {
		t.Skip("mke2fs not available, skipping the test")
	}

	var out, err bytes.Buffer
	cmd := exec.Command(cmdBin, "-F", "-t", fsType, path)
	cmd.Stderr = &err
	cmd.Stdout = &out
	cmdErr := cmd.Run()
	if cmdErr != nil {
		t.Fatalf("command failed: %s - stderr: %s - stdout: %s\n", cmdErr, err.String(), out.String())
	}
}

// createFullVirtualBlockDevice creates a full virtual
// block device with a file system in it.
// Warning the file will be earased!!!!
// @param[in] test handle to control the test execution
// @param[in] path to the virtual block device to be created.
// @param[in] type of the file system to be created in the virtual block device (e.g., "ext3")
func createFullVirtualBlockDevice(t *testing.T, path string, fsType string) {
	createVirtualBlockDevice(t, path)
	createFS(t, fsType, path)
}

// ext3InitializerTest prepares the initializer test by
// creating a new virtual block device that will be
// associated with the image, opening the image, get stat
// information about the file associated to the image and
// finally calling the initializer.
// @param[in] test handle to control the test execution.
// @param[in] image handle, when calling, simply an initialized Image variable.
// @param[in] path to the file that will be used with the image; it does not need to exist when calling the function.
// @return the error handle returned by the initializer. We do *not* intend to analyze the handle in this function since only the caller knows if it is a valid or invalid test case
func ext3InitializerTest(t *testing.T, img *Image, path string, fsType string) error {
	createFullVirtualBlockDevice(t, path, fsType)

	var err error
	img.File, err = os.Open(path)
	if err != nil {
		t.Fatalf("cannot open file: %s\n", err)
	}

	fileinfo, err := img.File.Stat()
	if err != nil {
		t.Fatalf("cannot stat image: %s\n", err)
	}

	var ext3format ext3Format
	err = ext3format.initializer(img, fileinfo)
	// err is just to be returned and analyzed by the caller

	img.File.Close()

	return err
}

func TestCheckExt3Header(t *testing.T) {
	b := make([]byte, bufferSize)

	// Create a fake ext3 file
	path := t.TempDir() + "ext3.fs"

	createFullVirtualBlockDevice(t, path, "ext3")

	// Now load the image
	img, imgErr := os.Open(path)
	if imgErr != nil {
		t.Fatal("impossible to load image for testing")
	}
	defer img.Close()
	defer os.Remove(path)

	n, err := img.Read(b)
	if err != nil || n != bufferSize {
		t.Fatalf("cannot read the first %d bytes of the image\n", bufferSize)
	}

	_, checkErr := CheckExt3Header(b)
	if checkErr != nil {
		t.Fatalf("cannot check ext3 header of a valid image: %s\n", checkErr)
	}
}

func TestInitializer(t *testing.T) {
	test.DropPrivilege(t)
	defer test.ResetPrivilege(t)

	// Create a temporary image which is obviously an invalid ext3 image
	f, err := os.CreateTemp(t.TempDir(), "image-")
	if err != nil {
		t.Fatalf("cannot create temporary file: %s\n", err)
	}
	path := f.Name()
	// We do not use defer f.Close() since we will be manually
	// opening and closing the file for testing.
	f.Close()
	resolvedPath, err := ResolvePath(path)
	if err != nil {
		t.Fatalf("failed to retrieve path for %s: %s\n", path, err)
	}

	img := &Image{
		Path: resolvedPath,
		Name: "test",
	}

	img.Writable = true
	var ext3format ext3Format
	mode := ext3format.openMode(true)
	if mode != os.O_RDWR {
		t.Fatal("wrong mode returned")
	}
	img.File, err = os.OpenFile(resolvedPath, mode, 0)
	if err != nil {
		t.Fatalf("cannot open the image: %s\n", err)
	}
	defer img.File.Close()
	fileinfo, err := img.File.Stat()
	if err != nil {
		t.Fatalf("cannot stat image: %s\n", err)
	}

	// This test will fail because we did not set a valid ext3 FS yet
	err = ext3format.initializer(img, fileinfo)
	if err == nil {
		t.Fatal("initializer succeeded while expected to fail")
	}

	// Now we setup a valid ext3 FS and run a test again
	err = ext3InitializerTest(t, img, resolvedPath, "ext3")
	if err != nil {
		t.Fatalf("ext3 initializer test failed with a valid ext3 image: %s\n", err)
	}

	// We now run a test with ext2 to hit some other corner cases
	err = ext3InitializerTest(t, img, resolvedPath, "ext2")
	if err == nil {
		t.Fatal("ext3 initializer test succeeded with an ext2 image while expected to fail")
	}

	// We reformat the image with different file systems and see if we catch
	// the error
	_, lookErr := exec.LookPath("mkfs.fat")
	if lookErr == nil {
		err = ext3InitializerTest(t, img, resolvedPath, "vfat")
		if err == nil {
			t.Fatal("ext3 initializer test succeeded with a vfat image")
		}
	} else {
		t.Log("mkfs.fat command is not available, skipping the test...")
	}

	_, lookErr = exec.LookPath("mkfs.ext4")
	if lookErr == nil {
		err = ext3InitializerTest(t, img, resolvedPath, "ext4")
		if err == nil {
			t.Fatal("ext3 initializer test succeeded with a ext4 image while expected to fail")
		}
	} else {
		t.Log("mkfs.ext4 command is not available, skipping the test...")
	}

	// A small test to exercise openMode() when using read-only mode
	mode = ext3format.openMode(false)
	if mode != os.O_RDONLY {
		t.Fatal("wrong mode returned")
	}

	// Error case when a directory is passed in to initializer()
	path = t.TempDir()
	resolvedPath, err = ResolvePath(path)
	if err != nil {
		t.Fatalf("failed to retrieve path for %s: %s\n", resolvedPath, err)
	}
	img.Path = resolvedPath
	img.File, err = os.Open(resolvedPath)
	if err != nil {
		t.Fatalf("cannot open %s: %s\n", resolvedPath, err)
	}
	fileinfo, err = img.File.Stat()
	if err != nil {
		t.Fatalf("cannot stat image: %s\n", err)
	}
	if fileinfo.IsDir() == false {
		t.Fatalf("invalid fileinfo for %s\n", resolvedPath)
	}

	err = ext3format.initializer(img, fileinfo)
	if err == nil {
		t.Fatal("ext3 initializer succeeded with a directory while expected to fail")
	}
}
