package distrodef

import (
	"os"
	"path/filepath"
	"slices"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const testDefLocation = "../data/defs"

func TestLoadSimple(t *testing.T) {
	def, err := LoadImageDef([]string{testDefLocation}, "debian", "trixie", "qcow2")
	require.NoError(t, err)
	assert.NotEmpty(t, def.Packages)
}

func TestLoadFuzzy(t *testing.T) {
	def, err := LoadImageDef([]string{testDefLocation}, "debian", "trixie", "qcow2")
	require.NoError(t, err)
	assert.NotEmpty(t, def.Packages)
}

func TestLoadUnhappy(t *testing.T) {
	_, err := LoadImageDef([]string{testDefLocation}, "ubuntu", "22.04", "qcow2")
	assert.ErrorContains(t, err, "could not find def file for distro ubuntu-22.04")
	
	_, err = LoadImageDef([]string{testDefLocation}, "debian", "0", "qcow2")
	assert.ErrorContains(t, err, "could not find def file for distro debian-0")

	_, err = LoadImageDef([]string{testDefLocation}, "debian", "trixie", "unsupported-type")
	assert.ErrorContains(t, err, "could not find def for distro debian and image type unsupported-type")

	_, err = LoadImageDef([]string{testDefLocation}, "debian", "xxx", "qcow2")
	assert.ErrorContains(t, err, `cannot parse wanted version string: `)
}

const fakeDefFileContent = `qcow2:
  packages:
    - base-files
    - systemd
    - linux-image-amd64
    - grub-common
    - bootc
    - apt-ostree

ami:
  packages:
    - base-files
    - systemd
    - linux-image-amd64
    - grub-common
    - cloud-guest-utils
    - bootc
    - apt-ostree

vmdk:
  packages:
    - base-files
    - systemd
    - linux-image-amd64
    - grub-common
    - open-vm-tools
    - bootc
    - apt-ostree

debian-installer:
  packages:
    - base-files
    - systemd
    - linux-image-amd64
    - debian-installer
    - bootc
    - apt-ostree

calamares:
  packages:
    - base-files
    - systemd
    - linux-image-amd64
    - calamares
    - bootc
    - apt-ostree
`

func makeFakeDistrodefRoot(t *testing.T, defFiles []string) (searchPaths []string) {
	tmp := t.TempDir()

	for _, defFile := range defFiles {
		p := filepath.Join(tmp, defFile)
		err := os.MkdirAll(filepath.Dir(p), 0755)
		require.NoError(t, err)
		err = os.WriteFile(p, []byte(fakeDefFileContent), 0644)
		require.NoError(t, err)

		if !slices.Contains(searchPaths, filepath.Dir(p)) {
			searchPaths = append(searchPaths, filepath.Dir(p))
		}
	}

	return searchPaths
}

func TestFindDistroDefMultiDirs(t *testing.T) {
	defDirs := makeFakeDistrodefRoot(t, []string{
		"a/debian-12.yaml",
		"b/debian-13.yaml",
		"c/debian-13.yaml",
	})
	assert.Equal(t, 3, len(defDirs))

	def, err := findDistroDef(defDirs, "debian", "13")
	assert.NoError(t, err)
	assert.True(t, strings.HasSuffix(def, "b/debian-13.yaml"))
}

func TestFindDistroDefMultiDirsIgnoreENOENT(t *testing.T) {
	defDirs := makeFakeDistrodefRoot(t, []string{
		"a/debian-13.yaml",
	})
	defDirs = append([]string{"/no/such/path"}, defDirs...)

	def, err := findDistroDef(defDirs, "debian", "13")
	assert.NoError(t, err)
	assert.True(t, strings.HasSuffix(def, "a/debian-13.yaml"))
}

func TestFindDistroDefMultiFuzzy(t *testing.T) {
	defDirs := makeFakeDistrodefRoot(t, []string{
		"a/debian-12.yaml",
		"b/debian-13.yaml",
		"b/b/debian-14.yaml",
		"c/debian-13.yaml",
	})
	// no debian-99, pick the closest
	def, err := findDistroDef(defDirs, "debian", "99")
	assert.NoError(t, err)
	assert.True(t, strings.HasSuffix(def, "b/b/debian-14.yaml"))
}

func TestFindDistroDefMultiFuzzyMinorReleases(t *testing.T) {
	defDirs := makeFakeDistrodefRoot(t, []string{
		"a/debian-12.1.yaml",
		"b/debian-11.yaml",
		"c/debian-13.1.yaml",
		"d/debian-13.1.1.yaml",
		"b/b/debian-13.10.yaml",
	})
	def, err := findDistroDef(defDirs, "debian", "13.11")
	assert.NoError(t, err)
	assert.True(t, strings.HasSuffix(def, "b/b/debian-13.10.yaml"), def)
}

func TestFindDistroDefMultiFuzzyMinorReleasesIsZero(t *testing.T) {
	defDirs := makeFakeDistrodefRoot(t, []string{
		"a/debian-13.yaml",
		"a/debian-14.yaml",
	})
	def, err := findDistroDef(defDirs, "debian", "14.0")
	assert.NoError(t, err)
	assert.True(t, strings.HasSuffix(def, "a/debian-14.yaml"), def)
}

func TestFindDistroDefMultiFuzzyError(t *testing.T) {
	defDirs := makeFakeDistrodefRoot(t, []string{
		"a/debian-13.yaml",
	})
	// the best version we have is newer than what is requested, this
	// is an error
	_, err := findDistroDef(defDirs, "debian", "10")
	assert.ErrorContains(t, err, "could not find def file for distro debian-10")
}

func TestFindDistroDefBadNumberIgnoresBadFiles(t *testing.T) {
	defDirs := makeFakeDistrodefRoot(t, []string{
		"a/debian-NaN.yaml",
	})
	_, err := findDistroDef(defDirs, "debian", "13")
	assert.ErrorContains(t, err, "could not find def file for distro debian-13")
}

func TestFindDistroDefCornerCases(t *testing.T) {
	defDirs := makeFakeDistrodefRoot(t, []string{
		"a/debian-.yaml",
		"b/debian-1.yaml",
		"c/debian.yaml",
	})
	def, err := findDistroDef(defDirs, "debian", "2")
	assert.NoError(t, err)
	assert.True(t, strings.HasSuffix(def, "b/debian-1.yaml"))
}

func TestLoadImageDefWithDifferentImageTypes(t *testing.T) {
	defDirs := makeFakeDistrodefRoot(t, []string{
		"a/debian-13.yaml",
	})

	// Test qcow2 image type
	def, err := LoadImageDef(defDirs, "debian", "trixie", "qcow2")
	assert.NoError(t, err)
	assert.NotEmpty(t, def.Packages)
	assert.Contains(t, def.Packages, "base-files")
	assert.Contains(t, def.Packages, "systemd")
	assert.Contains(t, def.Packages, "bootc")

	// Test ami image type
	def, err = LoadImageDef(defDirs, "debian", "13", "ami")
	assert.NoError(t, err)
	assert.NotEmpty(t, def.Packages)
	assert.Contains(t, def.Packages, "cloud-guest-utils")

	// Test vmdk image type
	def, err = LoadImageDef(defDirs, "debian", "13", "vmdk")
	assert.NoError(t, err)
	assert.NotEmpty(t, def.Packages)
	assert.Contains(t, def.Packages, "open-vm-tools")

	// Test debian-installer image type
	def, err = LoadImageDef(defDirs, "debian", "13", "debian-installer")
	assert.NoError(t, err)
	assert.NotEmpty(t, def.Packages)
	assert.Contains(t, def.Packages, "debian-installer")

	// Test calamares image type
	def, err = LoadImageDef(defDirs, "debian", "13", "calamares")
	assert.NoError(t, err)
	assert.NotEmpty(t, def.Packages)
	assert.Contains(t, def.Packages, "calamares")
}

func TestLoadImageDefWithCodenames(t *testing.T) {
	defDirs := makeFakeDistrodefRoot(t, []string{
		"a/debian-trixie.yaml",
		"b/debian-bookworm.yaml",
		"c/debian-sid.yaml",
	})

	// Test with codename
	def, err := LoadImageDef(defDirs, "debian", "trixie", "qcow2")
	assert.NoError(t, err)
	assert.NotEmpty(t, def.Packages)

	// Test with numeric version
	def, err = LoadImageDef(defDirs, "debian", "13", "qcow2")
	assert.NoError(t, err)
	assert.NotEmpty(t, def.Packages)

	// Test with stable codename
	def, err = LoadImageDef(defDirs, "debian", "stable", "qcow2")
	assert.NoError(t, err)
	assert.NotEmpty(t, def.Packages)
}
