package ldsocache

import (
	"errors"
	"io"
	"os"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/require"
	"gopkg.in/yaml.v3"
)

func Test_LoadCacheFile(t *testing.T) {
	f, err := os.Open("testdata/ld.so.cache")
	require.NoError(t, err)
	cacheFile, err := LoadCacheFile(f)
	require.NoError(t, err)
	f.Close()
	require.Equalf(t, uint32(65), cacheFile.Header.NumLibs, "there should be 65 libraries in this cache file")
	require.Equalf(t, uint32(1421), cacheFile.Header.StrTableSize, "the string table should be 1421 bytes long")
	require.Equalf(t, 1, len(cacheFile.Extensions), "there must be 1 extension")

	ext := cacheFile.Extensions[0]
	require.Equalf(t, uint32(0), ext.Header.Tag, "extension data must be tag 0 (generator)")
	require.Equalf(t, []byte("ldconfig (GNU libc) stable release version 2.36"), ext.Data, "must be generated by glibc 2.36")
}

func Test_WriteCacheFile(t *testing.T) {
	f, err := os.Open("testdata/ld.so.cache")
	require.NoError(t, err)
	cacheFile, err := LoadCacheFile(f)
	require.NoError(t, err)
	f.Close()
	out, err := os.CreateTemp(t.TempDir(), "ld.so.cache-new")
	require.NoError(t, err)
	err = cacheFile.Write(out)
	require.NoError(t, err)
}

func Test_ParseLibFilename(t *testing.T) {
	name, ver, err := ParseLibFilename("libfoo.so.1")
	require.NoError(t, err)
	require.Equal(t, name, "libfoo")
	require.Equal(t, ver, "1")
}

func Test_ParseLibFilename_Versioned_DotOne(t *testing.T) {
	name, ver, err := ParseLibFilename("libfoo.so.1")
	require.NoError(t, err)
	require.Equal(t, name, "libfoo")
	require.Equal(t, ver, "1")
}

func Test_ParseLibFilename_Versioned_DotOneDotTwo(t *testing.T) {
	name, ver, err := ParseLibFilename("libfoo.so.1.2")
	require.NoError(t, err)
	require.Equal(t, name, "libfoo")
	require.Equal(t, ver, "1.2")
}

func Test_ParseLibFilename_Versioned_SoSo_DotOne(t *testing.T) {
	name, ver, err := ParseLibFilename("libso.so.1")
	require.NoError(t, err)
	require.Equal(t, name, "libso")
	require.Equal(t, ver, "1")
}

func Test_ParseLibFilename_Unversioned_SoSo(t *testing.T) {
	name, ver, err := ParseLibFilename("libso.so")
	require.NoError(t, err)
	require.Equal(t, name, "libso")
	require.Equal(t, ver, "")
}

func Test_ParseLibFilename_Unversioned_SoDotSoDotSo(t *testing.T) {
	name, ver, err := ParseLibFilename("libso.so.so")
	require.NoError(t, err)
	require.Equal(t, name, "libso.so")
	require.Equal(t, ver, "")
}

func Test_ParseLibFilename_Versioned_SoDotSoDotSoVer(t *testing.T) {
	name, ver, err := ParseLibFilename("libso.so.so.7")
	require.NoError(t, err)
	require.Equal(t, name, "libso.so")
	require.Equal(t, ver, "7")
}

func Test_ParseLibFilename_HangingSo(t *testing.T) {
	// Unclear if this should be an error
	name, ver, err := ParseLibFilename("libfoo.so.")
	require.NoError(t, err)
	require.Equal(t, name, "libfoo")
	require.Equal(t, ver, "")
}

func Test_ParseLibFilename_Versioned_NoName(t *testing.T) {
	// Unclear if this should be an error
	name, ver, err := ParseLibFilename("lib.so")
	require.NoError(t, err)
	require.Equal(t, name, "lib")
	require.Equal(t, ver, "")
}

func Test_ParseLibFilename_NoLibPrefix(t *testing.T) {
	_, _, err := ParseLibFilename("foo.so.1")
	require.Error(t, err)
}

func Test_ParseLibFilename_NoSo(t *testing.T) {
	_, _, err := ParseLibFilename("libfoo.no.1")
	require.Error(t, err)
}

func Test_ParseLDSOConf_Simple(t *testing.T) {
	fsys := os.DirFS("testdata")
	dirs, err := ParseLDSOConf(fsys, "ld.so.conf.simple")
	require.NoError(t, err)
	require.Equal(t, 1, len(dirs))
	require.Equal(t, "/lib", dirs[0])
}

func Test_ParseLDSOConf_Glob(t *testing.T) {
	fsys := os.DirFS("testdata")
	dirs, err := ParseLDSOConf(fsys, "ld.so.conf.glob")
	require.NoError(t, err)
	require.Contains(t, dirs, "/a/libs")
	require.Contains(t, dirs, "/b/libs")
}

// Instead of real ELF binaries, our "libraries" are YAML files
// that are used to populate an elfInfo structure.
func mockGetElfInfo(r io.ReaderAt) (elfInfo, error) {
	var info elfInfo
	buf := make([]byte, 1024)
	size, err := r.ReadAt(buf, 0)
	if !errors.Is(err, io.EOF) {
		return info, err
	}
	if size == 0 {
		return info, err
	}
	err = yaml.Unmarshal(buf[:size-1], &info)
	if err != nil {
		return info, err
	}
	return info, nil
}

func Test_GenerateCacheFile(t *testing.T) {
	getElfInfo = mockGetElfInfo
	root := os.DirFS("testdata/libroot")
	libdirs, err := ParseLDSOConf(root, "etc/ld.so.conf")
	require.NoError(t, err)
	expectedLibDirs := []string{
		"/usr/local/lib",
		"/usr/local/lib64",
		"/lib",
		"/lib64",
		"/usr/lib",
		"/usr/lib64",
		"/usr/local/sdk-v1/lib",
		"/usr/local/sdk-v2/lib",
	}
	require.ElementsMatch(t, expectedLibDirs, libdirs)
	cacheFile, err := BuildCacheFileForDirs(root, libdirs)
	require.NoError(t, err)
	lsc, err := os.CreateTemp(t.TempDir(), "ld.so.cache-generated")
	require.NoError(t, err)
	err = cacheFile.Write(lsc)
	require.NoError(t, err)
	lsc.Close()

	f, err := os.Open(lsc.Name())
	require.NoError(t, err)
	cacheFile, err = LoadCacheFile(f)
	require.NoError(t, err)
	f.Close()
	expectedLibs := []string{
		"/lib/libfoo.so.1",
		"/lib/libnosoname.so",
		"/lib64/libfoo.so.1",
		"/lib64/libnosoname.so",
		"/usr/local/lib/sdk-v1/libsdk.so.1",
		"/usr/local/lib/sdk-v2/libsdk.so.1",
		"/usr/local/lib/sdk-v1/libsdk.so",
		"/usr/local/lib/sdk-v2/libsdk.so",
	}
	expectedLibLen := len(expectedLibs)
	require.Equalf(t, uint32(expectedLibLen), cacheFile.Header.NumLibs, "there should be %d libraries in this cache file", expectedLibLen)
	for i := 1; i < int(cacheFile.Header.NumLibs); i++ {
		prev := filepath.Base(cacheFile.Entries[i-1].Name)
		cur := filepath.Base(cacheFile.Entries[i].Name)
		require.GreaterOrEqual(t, prev, cur)
	}
}
