// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package arch

import (
	"bytes"
	"io/fs"
	"os"
	"strings"
	"testing"
	"testing/fstest"
	"time"

	"forgejo.org/modules/packages"

	"github.com/mholt/archives"
	"github.com/stretchr/testify/require"
)

func TestParsePackage(t *testing.T) {
	// Minimal PKGINFO contents and test FS
	const PKGINFO = `pkgname = a
pkgbase = b
pkgver = 1-2
arch = x86_64
`
	mapfs := fstest.MapFS{
		"pkginfo": &fstest.MapFile{
			Data:    []byte(PKGINFO),
			Mode:    os.ModePerm,
			ModTime: time.Now(),
		},
		"mtree": &fstest.MapFile{
			Data:    []byte("data"),
			Mode:    os.ModePerm,
			ModTime: time.Now(),
		},
	}

	// Test .PKGINFO file
	pinf, err := mapfs.Stat("pkginfo")
	require.NoError(t, err)

	// Test .MTREE file
	minf, err := mapfs.Stat("mtree")
	require.NoError(t, err)

	files := []archives.FileInfo{
		{
			FileInfo:      pinf,
			NameInArchive: ".PKGINFO",
			Open: func() (fs.File, error) {
				return mapfs.Open("pkginfo")
			},
		},
		{
			FileInfo:      minf,
			NameInArchive: ".MTREE",
			Open: func() (fs.File, error) {
				return mapfs.Open("mtree")
			},
		},
	}

	t.Run("normal zst archive", func(t *testing.T) {
		var buf bytes.Buffer
		archive := archives.CompressedArchive{
			Archival:    archives.Tar{},
			Compression: archives.Zstd{},
		}
		archive.Archive(t.Context(), &buf, files)

		reader, err := packages.CreateHashedBufferFromReader(&buf)
		require.NoError(t, err)
		defer reader.Close()
		pkg, err := ParsePackage(reader)

		require.Equal(t, "zst", pkg.CompressType)
		require.Equal(t, "a", pkg.Name)
		require.Equal(t, "b", pkg.VersionMetadata.Base)
		require.Equal(t, "x86_64", pkg.FileMetadata.Arch)
		require.Equal(t, "1-2", pkg.Version)

		require.NoError(t, err)
	})

	t.Run("normal xz archive", func(t *testing.T) {
		var buf bytes.Buffer
		archive := archives.CompressedArchive{
			Archival:    archives.Tar{},
			Compression: archives.Xz{},
		}
		archive.Archive(t.Context(), &buf, files)

		reader, err := packages.CreateHashedBufferFromReader(&buf)
		require.NoError(t, err)
		defer reader.Close()
		pkg, err := ParsePackage(reader)

		require.Equal(t, "xz", pkg.CompressType)
		require.Equal(t, "a", pkg.Name)
		require.Equal(t, "b", pkg.VersionMetadata.Base)
		require.Equal(t, "x86_64", pkg.FileMetadata.Arch)
		require.Equal(t, "1-2", pkg.Version)

		require.NoError(t, err)
	})

	t.Run("normal gz archive", func(t *testing.T) {
		var buf bytes.Buffer
		archive := archives.CompressedArchive{
			Archival:    archives.Tar{},
			Compression: archives.Gz{},
		}
		archive.Archive(t.Context(), &buf, files)

		reader, err := packages.CreateHashedBufferFromReader(&buf)
		require.NoError(t, err)
		defer reader.Close()
		pkg, err := ParsePackage(reader)

		require.Equal(t, "gz", pkg.CompressType)
		require.Equal(t, "a", pkg.Name)
		require.Equal(t, "b", pkg.VersionMetadata.Base)
		require.Equal(t, "x86_64", pkg.FileMetadata.Arch)
		require.Equal(t, "1-2", pkg.Version)

		require.NoError(t, err)
	})

	t.Run("missing .PKGINFO", func(t *testing.T) {
		var buf bytes.Buffer
		archive := archives.CompressedArchive{
			Archival:    archives.Tar{},
			Compression: archives.Zstd{},
		}
		archive.Archive(t.Context(), &buf, []archives.FileInfo{
			{
				FileInfo:      minf,
				NameInArchive: ".MTREE",
				Open: func() (fs.File, error) {
					return mapfs.Open("mtree")
				},
			},
		})

		reader, err := packages.CreateHashedBufferFromReader(&buf)
		require.NoError(t, err)

		defer reader.Close()
		_, err = ParsePackage(reader)

		require.Error(t, err)
		require.Contains(t, err.Error(), ".PKGINFO file not found")
	})

	t.Run("missing .MTREE", func(t *testing.T) {
		var buf bytes.Buffer
		archive := archives.CompressedArchive{
			Archival:    archives.Tar{},
			Compression: archives.Zstd{},
		}
		archive.Archive(t.Context(), &buf, []archives.FileInfo{
			{
				FileInfo:      pinf,
				NameInArchive: ".PKGINFO",
				Open: func() (fs.File, error) {
					return mapfs.Open("pkginfo")
				},
			},
		})

		reader, err := packages.CreateHashedBufferFromReader(&buf)
		require.NoError(t, err)

		defer reader.Close()
		_, err = ParsePackage(reader)

		require.Error(t, err)
		require.Contains(t, err.Error(), ".MTREE file not found")
	})
}

func TestParsePackageInfo(t *testing.T) {
	const PKGINFO = `# Generated by makepkg 6.0.2
# using fakeroot version 1.31
pkgname = a
pkgbase = b
pkgver = 1-2
pkgdesc = comment
url = https://example.com/
group = group
builddate = 3
packager = Name Surname <login@example.com>
size = 5
arch = x86_64
license = BSD
provides = pvd
depend = smth
optdepend = hex
checkdepend = ola
makedepend = cmake
backup = usr/bin/paket1
`
	p, err := ParsePackageInfo("zst", strings.NewReader(PKGINFO))
	require.NoError(t, err)
	require.Equal(t, Package{
		CompressType: "zst",
		Name:         "a",
		Version:      "1-2",
		VersionMetadata: VersionMetadata{
			Base:         "b",
			Description:  "comment",
			ProjectURL:   "https://example.com/",
			Groups:       []string{"group"},
			Provides:     []string{"pvd"},
			License:      []string{"BSD"},
			Depends:      []string{"smth"},
			OptDepends:   []string{"hex"},
			MakeDepends:  []string{"cmake"},
			CheckDepends: []string{"ola"},
			Backup:       []string{"usr/bin/paket1"},
		},
		FileMetadata: FileMetadata{
			InstalledSize: 5,
			BuildDate:     3,
			Packager:      "Name Surname <login@example.com>",
			Arch:          "x86_64",
		},
	}, *p)
}

func TestValidatePackageSpec(t *testing.T) {
	newpkg := func() Package {
		return Package{
			Name:    "abc",
			Version: "1-1",
			VersionMetadata: VersionMetadata{
				Base:         "ghx",
				Description:  "whoami",
				ProjectURL:   "https://example.com/",
				Groups:       []string{"gnome"},
				Provides:     []string{"abc", "def"},
				License:      []string{"GPL"},
				Depends:      []string{"go", "gpg=1", "curl>=3", "git<=7"},
				OptDepends:   []string{"git", "libgcc=1.0", "gzip>1.0", "gz>=1.0", "lz<1.0", "gzip<=1.0", "zstd>1.0:foo bar<test>"},
				MakeDepends:  []string{"chrom"},
				CheckDepends: []string{"bariy"},
				Backup:       []string{"etc/pacman.d/filo"},
			},
			FileMetadata: FileMetadata{
				CompressedSize: 1,
				InstalledSize:  2,
				SHA256:         "def",
				BuildDate:      3,
				Packager:       "smon",
				Arch:           "x86_64",
			},
		}
	}

	t.Run("valid package", func(t *testing.T) {
		p := newpkg()

		err := ValidatePackageSpec(&p)

		require.NoError(t, err)
	})

	t.Run("invalid package name", func(t *testing.T) {
		p := newpkg()
		p.Name = "!$%@^!*&()"

		err := ValidatePackageSpec(&p)

		require.Error(t, err)
		require.Contains(t, err.Error(), "invalid package name")
	})

	t.Run("invalid package base", func(t *testing.T) {
		p := newpkg()
		p.VersionMetadata.Base = "!$%@^!*&()"

		err := ValidatePackageSpec(&p)

		require.Error(t, err)
		require.Contains(t, err.Error(), "invalid package base")
	})

	t.Run("invalid package version", func(t *testing.T) {
		p := newpkg()
		p.VersionMetadata.Base = "una-luna?"

		err := ValidatePackageSpec(&p)

		require.Error(t, err)
		require.Contains(t, err.Error(), "invalid package base")
	})

	t.Run("invalid package version", func(t *testing.T) {
		p := newpkg()
		p.Version = "una-luna"

		err := ValidatePackageSpec(&p)

		require.Error(t, err)
		require.Contains(t, err.Error(), "invalid package version")
	})

	t.Run("missing architecture", func(t *testing.T) {
		p := newpkg()
		p.FileMetadata.Arch = ""

		err := ValidatePackageSpec(&p)

		require.Error(t, err)
		require.Contains(t, err.Error(), "architecture should be specified")
	})

	t.Run("invalid URL", func(t *testing.T) {
		p := newpkg()
		p.VersionMetadata.ProjectURL = "http%%$#"

		err := ValidatePackageSpec(&p)

		require.Error(t, err)
		require.Contains(t, err.Error(), "invalid project URL")
	})

	t.Run("invalid check dependency", func(t *testing.T) {
		p := newpkg()
		p.VersionMetadata.CheckDepends = []string{"Err^_^"}

		err := ValidatePackageSpec(&p)

		require.Error(t, err)
		require.Contains(t, err.Error(), "invalid check dependency")
	})

	t.Run("invalid dependency", func(t *testing.T) {
		p := newpkg()
		p.VersionMetadata.Depends = []string{"^^abc"}

		err := ValidatePackageSpec(&p)

		require.Error(t, err)
		require.Contains(t, err.Error(), "invalid dependency")
	})

	t.Run("invalid make dependency", func(t *testing.T) {
		p := newpkg()
		p.VersionMetadata.MakeDepends = []string{"^m^"}

		err := ValidatePackageSpec(&p)

		require.Error(t, err)
		require.Contains(t, err.Error(), "invalid make dependency")
	})

	t.Run("invalid provides", func(t *testing.T) {
		p := newpkg()
		p.VersionMetadata.Provides = []string{"^m^"}

		err := ValidatePackageSpec(&p)

		require.Error(t, err)
		require.Contains(t, err.Error(), "invalid provides")
	})

	t.Run("invalid optional dependency", func(t *testing.T) {
		p := newpkg()
		p.VersionMetadata.OptDepends = []string{"^m^:MM"}

		err := ValidatePackageSpec(&p)

		require.Error(t, err)
		require.Contains(t, err.Error(), "invalid optional dependency")
	})

	t.Run("invalid optional dependency", func(t *testing.T) {
		p := newpkg()
		p.VersionMetadata.Backup = []string{"/ola/cola"}

		err := ValidatePackageSpec(&p)

		require.Error(t, err)
		require.Contains(t, err.Error(), "backup file contains leading forward slash")
	})
}

func TestDescAndFileString(t *testing.T) {
	const pkgDesc = `%FILENAME%
zstd-1.5.5-1-x86_64.pkg.tar.zst

%NAME%
zstd

%BASE%
zstd

%VERSION%
1.5.5-1

%DESC%
Zstandard - Fast real-time compression algorithm

%GROUPS%
dummy1
dummy2

%CSIZE%
401

%ISIZE%
1500453

%MD5SUM%
5016660ef3d9aa148a7b72a08d3df1b2

%SHA256SUM%
9fa4ede47e35f5971e4f26ecadcbfb66ab79f1d638317ac80334a3362dedbabd

%URL%
https://facebook.github.io/zstd/

%LICENSE%
BSD
GPL2

%ARCH%
x86_64

%BUILDDATE%
1681646714

%PACKAGER%
Jelle van der Waa <jelle@archlinux.org>

%PROVIDES%
libzstd.so=1-64

%DEPENDS%
glibc
gcc-libs
zlib
xz
lz4

%OPTDEPENDS%
dummy3
dummy4

%MAKEDEPENDS%
cmake
gtest
ninja

%CHECKDEPENDS%
dummy5
dummy6

`

	const pkgFiles = `%FILES%
usr/
usr/bin/
usr/bin/zstd
`

	md := &Package{
		CompressType: "zst",
		Name:         "zstd",
		Version:      "1.5.5-1",
		VersionMetadata: VersionMetadata{
			Base:         "zstd",
			Description:  "Zstandard - Fast real-time compression algorithm",
			ProjectURL:   "https://facebook.github.io/zstd/",
			Groups:       []string{"dummy1", "dummy2"},
			Provides:     []string{"libzstd.so=1-64"},
			License:      []string{"BSD", "GPL2"},
			Depends:      []string{"glibc", "gcc-libs", "zlib", "xz", "lz4"},
			OptDepends:   []string{"dummy3", "dummy4"},
			MakeDepends:  []string{"cmake", "gtest", "ninja"},
			CheckDepends: []string{"dummy5", "dummy6"},
		},
		FileMetadata: FileMetadata{
			CompressedSize: 401,
			InstalledSize:  1500453,
			MD5:            "5016660ef3d9aa148a7b72a08d3df1b2",
			SHA256:         "9fa4ede47e35f5971e4f26ecadcbfb66ab79f1d638317ac80334a3362dedbabd",
			BuildDate:      1681646714,
			Packager:       "Jelle van der Waa <jelle@archlinux.org>",
			Arch:           "x86_64",
			Files:          []string{"usr/", "usr/bin/", "usr/bin/zstd"},
		},
	}
	require.Equal(t, pkgDesc, md.Desc())
	require.Equal(t, pkgFiles, md.Files())
}
