// SPDX-FileCopyrightText: 2024 Nicolas Peugnet <nicolas@club1.fr>
// SPDX-License-Identifier: GPL-2.0-or-later

package lintian_test

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"reflect"
	"testing"

	"salsa.debian.org/lintian/lintian-ssg/lintian"
)

const baseURL = "https://salsa.debian.org/lintian/lintian/-/blob//tags/"

func TestRenamedFromStr(t *testing.T) {
	tag := lintian.Tag{RenamedFrom: []string{"test-tag", "another-one"}}
	expected := "test-tag, another-one"
	actual := tag.RenamedFromStr()
	if actual != expected {
		t.Errorf("expected: '%v' actual: '%v'", expected, actual)
	}
}

func TestSource(t *testing.T) {
	cases := []struct {
		tag      lintian.Tag
		expected string
	}{
		{ // Basic case
			lintian.Tag{Name: "test-tag", NameSpaced: false},
			"t/test-tag.tag",
		},
		{ // namespaced case
			lintian.Tag{Name: "teams/js/test-tag", NameSpaced: true},
			"teams/js/test-tag.tag",
		},
	}
	for i, c := range cases {
		t.Run(fmt.Sprintf("%d %s", i, c.expected), func(t *testing.T) {
			expected := baseURL + c.expected
			actual := c.tag.Source()
			if actual != expected {
				t.Fatalf("\nexpected: %q\nactual  : %q", expected, actual)
			}
		})
	}
}

func TestSeeAlsoHTML(t *testing.T) {
	tag := lintian.Tag{
		SeeAlso: []string{
			"[File System Structure](https://www.debian.org/doc/debian-policy/ch-opersys.html#file-system-structure) (Section 9.1.1) in the Debian Policy Manual",
			"filesystem-hierarchy",
			"<https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch04s07.html>",
			"[Bug#954149](https://bugs.debian.org/954149)",
		},
	}
	expected := []string{
		"<p><a href=\"https://www.debian.org/doc/debian-policy/ch-opersys.html#file-system-structure\">File System Structure</a> (Section 9.1.1) in the Debian Policy Manual</p>\n",
		"<p>filesystem-hierarchy</p>\n",
		"<p><a href=\"https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch04s07.html\">https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch04s07.html</a></p>\n",
		"<p><a href=\"https://bugs.debian.org/954149\">Bug#954149</a></p>\n",
	}
	actual := tag.SeeAlsoHTML()
	for i, a := range actual {
		if string(a) != expected[i] {
			t.Errorf("\nexpected:\n%v actual:\n%v", expected[i], a)
		}
	}
}

func TestUnmarshal(t *testing.T) {
	cases := []struct {
		tag      string
		expected lintian.Tag
	}{
		{ // lintian v2.118.0
			"lintian_2.118.0_executable-in-usr-lib",
			lintian.Tag{
				Experimental:   true,
				Explanation:    "The package ships an executable file in /usr/lib.\n\nPlease move the file to <code>/usr/libexec</code>.\n\nWith policy revision 4.1.5, Debian adopted the Filesystem\nHierarchy Specification (FHS) version 3.0.\n\nThe FHS 3.0 describes <code>/usr/libexec</code>. Please use that\nlocation for executables.",
				LintianVersion: "2.118.0",
				Name:           "executable-in-usr-lib",
				NameSpaced:     false,
				Screens: []lintian.Screen{
					{
						Advocates: []string{"\"David Bremner\" <bremner@debian.org>"},
						Name:      "emacs/elpa/scripts",
						Reason:    "The <code>emacsen-common</code> package places installation\nand removal scripts, which for ELPA packages are executable,\nin the folder <code>/usr/lib/emacsen-common/packages</code>.\n\nAbout four hundred installation packages are affected. All of\nthem declare <code>emacsen-common</code> as an installation\nprerequisite.",
						SeeAlso: []string{
							"[Bug#974175](https://bugs.debian.org/974175)",
							"[Bug#954149](https://bugs.debian.org/954149)",
						},
					},
					{
						Advocates: []string{"\"Andrius Merkys\" <merkys@debian.org>"},
						Name:      "web/cgi/scripts",
						Reason:    "The folder <code>/usr/lib/cgi-bin/</code> is designated for\nscripts in the Common Gateway Interface (CGI). They require the\nexecutable bit so the server can run them.",
						SeeAlso: []string{
							"<https://en.wikipedia.org/wiki/Common_Gateway_Interface>",
							"<https://datatracker.ietf.org/doc/html/rfc3875.html>",
							"[Bug#1003941](https://bugs.debian.org/1003941)",
						},
					},
				},
				SeeAlso: []string{
					"[File System Structure](https://www.debian.org/doc/debian-policy/ch-opersys.html#file-system-structure) (Section 9.1.1) in the Debian Policy Manual",
					"filesystem-hierarchy",
					"<https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch04s07.html>",
					"[Bug#954149](https://bugs.debian.org/954149)",
				},
				Visibility: lintian.LevelPedantic,
			},
		},
	}
	for i, c := range cases {
		t.Run(fmt.Sprintf("%d %s", i, c.tag), func(t *testing.T) {
			file, err := os.Open(filepath.Join("testdata", c.tag+".json"))
			if err != nil {
				t.Fatal(err)
			}
			var actual lintian.Tag
			decoder := json.NewDecoder(file)
			if err := decoder.Decode(&actual); err != nil {
				t.Fatal(err)
			}
			if reflect.DeepEqual(c.expected, actual) {
				t.Fatalf("\nexpected: %v\nactual  : %v", c.expected, actual)
			}
		})
	}
}

func TestScreenSeeAlsoHTML(t *testing.T) {
	screen := lintian.Screen{SeeAlso: []string{
		"<https://debian.org>",
		"[Bug#954149](https://bugs.debian.org/954149)",
	}}
	expected := "<p>See also: <a href=\"https://debian.org\">https://debian.org</a>, <a href=\"https://bugs.debian.org/954149\">Bug#954149</a></p>\n"
	actual := screen.SeeAlsoHTML()
	if string(actual) != expected {
		t.Errorf("\nexpected:\n%v\nactual:\n%v", expected, actual)
	}
}

var tagsProviderCases = []struct {
	json     string
	expected []lintian.Tag
}{
	{`[{"name":"t1"}]`, []lintian.Tag{{Name: "t1"}}},
	{`[{"name":"t1"},{"name":"t2"},{"name":"t3"}]`, []lintian.Tag{{Name: "t1"}, {Name: "t2"}, {Name: "t3"}}},
}

const lintianExplainTagsFmt = `#!/bin/sh
if test "$1" = "--format=json"
then
	echo %q
fi
`

func setupTagsProg(t *testing.T, content string) {
	checkErr := func(err error) {
		if err != nil {
			t.Fatal(err)
		}
	}
	tmp := t.TempDir()
	binFile, err := os.Create(filepath.Join(tmp, "lintian-explain-tags"))
	checkErr(err)
	defer binFile.Close()
	checkErr(binFile.Chmod(0755))
	_, err = fmt.Fprintf(binFile, lintianExplainTagsFmt, content)
	checkErr(err)
	t.Setenv("PATH", tmp+":"+os.Getenv("PATH"))
}

func TestSystemTagsProviderBasic(t *testing.T) {
	for _, c := range tagsProviderCases {
		t.Run(c.json, func(t *testing.T) {
			setupTagsProg(t, c.json)
			var actual []lintian.Tag
			p, err := lintian.NewSystemTagsProvider()
			if err != nil {
				t.Fatal("unexpected exec error: ", err)
			}
			err = p.ForEach(func(tag *lintian.Tag) {
				actual = append(actual, *tag)
			})
			if err != nil {
				t.Fatal("unexpected decode error: ", err)
			}
			if !reflect.DeepEqual(c.expected, actual) {
				t.Errorf("expected: %+v, got: %+v", c.expected, actual)
			}
			_, err = p.Wait()
			if err != nil {
				t.Fatal("unexpected wait error: ", err)
			}
		})
	}
}

func setupTagsFile(t *testing.T, content string) string {
	tmp := t.TempDir()
	tagsFile := filepath.Join(tmp, "tags")
	if err := os.WriteFile(tagsFile, []byte(content), 0644); err != nil {
		t.Fatal(err)
	}
	return tagsFile
}

func TestFileTagsProviderBasic(t *testing.T) {
	for _, c := range tagsProviderCases {
		t.Run(c.json, func(t *testing.T) {
			var actual []lintian.Tag
			p, err := lintian.NewFileTagsProvider(setupTagsFile(t, c.json))
			if err != nil {
				t.Fatal("unexpected open error: ", err)
			}
			err = p.ForEach(func(tag *lintian.Tag) {
				actual = append(actual, *tag)
			})
			if err != nil {
				t.Fatal("unexpected decode error: ", err)
			}
			if !reflect.DeepEqual(c.expected, actual) {
				t.Errorf("expected: %+v, got: %+v", c.expected, actual)
			}
			_, err = p.Wait()
			if err != nil {
				t.Fatal("unexpected wait error: ", err)
			}
		})
	}
}

func TestTagsProvidersDecodeErrors(t *testing.T) {
	cases := []string{
		``,
		`{"name":"t1"}]`,
		`[{"name":"t1"},{"name":"t2"},{"name":"t3"}`,
	}
	for _, c := range cases {
		t.Run(c, func(t *testing.T) {
			p, err := lintian.NewFileTagsProvider(setupTagsFile(t, c))
			if err != nil {
				t.Fatal("unexpected open error: ", err)
			}
			err = p.ForEach(func(tag *lintian.Tag) {})
			if err == nil {
				t.Fatal("expected decode error: ", err)
			}
		})
	}
}
