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

package arch

import (
	"bytes"
	"errors"
	"os"
	"strings"
	"testing"
	"testing/fstest"
	"time"

	"forgejo.org/modules/packages"

	"github.com/mholt/archiver/v3"
	"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
`
	fs := 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 := fs.Stat("pkginfo")
	require.NoError(t, err)

	pfile, err := fs.Open("pkginfo")
	require.NoError(t, err)

	parcname, err := archiver.NameInArchive(pinf, ".PKGINFO", ".PKGINFO")
	require.NoError(t, err)

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

	mfile, err := fs.Open("mtree")
	require.NoError(t, err)

	marcname, err := archiver.NameInArchive(minf, ".MTREE", ".MTREE")
	require.NoError(t, err)

	t.Run("normal archive", func(t *testing.T) {
		var buf bytes.Buffer

		archive := archiver.NewTarZstd()
		archive.Create(&buf)

		err = archive.Write(archiver.File{
			FileInfo: archiver.FileInfo{
				FileInfo:   pinf,
				CustomName: parcname,
			},
			ReadCloser: pfile,
		})
		require.NoError(t, errors.Join(pfile.Close(), err))

		err = archive.Write(archiver.File{
			FileInfo: archiver.FileInfo{
				FileInfo:   minf,
				CustomName: marcname,
			},
			ReadCloser: mfile,
		})
		require.NoError(t, errors.Join(mfile.Close(), archive.Close(), err))

		reader, err := packages.CreateHashedBufferFromReader(&buf)
		if err != nil {
			t.Fatal(err)
		}
		defer reader.Close()
		_, err = ParsePackage(reader)

		require.NoError(t, err)
	})

	t.Run("missing .PKGINFO", func(t *testing.T) {
		var buf bytes.Buffer

		archive := archiver.NewTarZstd()
		archive.Create(&buf)
		require.NoError(t, archive.Close())

		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

		pfile, err := fs.Open("pkginfo")
		require.NoError(t, err)

		archive := archiver.NewTarZstd()
		archive.Create(&buf)

		err = archive.Write(archiver.File{
			FileInfo: archiver.FileInfo{
				FileInfo:   pinf,
				CustomName: parcname,
			},
			ReadCloser: pfile,
		})
		require.NoError(t, errors.Join(pfile.Close(), archive.Close(), err))
		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())
}
