package configfile

import (
	"io"
	"io/fs"
	"iter"
	"os"
	"path/filepath"
	"testing"

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

func Test_getDropInPathsUnderMain(t *testing.T) {
	tests := []struct {
		name string
		// Arguments for this function
		mainPath string
		suffix   string
		uid      int
		// Expected result
		want []string
	}{
		{
			name:     "basic rootful",
			mainPath: "/etc/containers/containers.conf",
			suffix:   ".conf",
			uid:      0,
			want:     []string{"/etc/containers/containers.rootful.conf.d", "/etc/containers/containers.conf.d"},
		},
		{
			name:     "basic rootless uid 500",
			mainPath: "/etc/containers/containers.conf",
			suffix:   ".conf",
			uid:      500,
			want:     []string{"/etc/containers/containers.rootless.conf.d/500", "/etc/containers/containers.rootless.conf.d", "/etc/containers/containers.conf.d"},
		},
		{
			name:     "basic rootless uid 1234",
			mainPath: "/etc/containers/containers.conf",
			suffix:   ".conf",
			uid:      1234,
			want:     []string{"/etc/containers/containers.rootless.conf.d/1234", "/etc/containers/containers.rootless.conf.d", "/etc/containers/containers.conf.d"},
		},
		{
			name:     "path with extra dots",
			mainPath: "/path.with.dots/containers.conf",
			suffix:   ".conf",
			uid:      0,
			want:     []string{"/path.with.dots/containers.rootful.conf.d", "/path.with.dots/containers.conf.d"},
		},
		{
			name:     "/usr rootful",
			mainPath: "/usr/share/containers/containers.conf",
			suffix:   ".conf",
			uid:      0,
			want:     []string{"/usr/share/containers/containers.rootful.conf.d", "/usr/share/containers/containers.conf.d"},
		},
		{
			name:     "storage.conf",
			mainPath: "/usr/share/containers/storage.conf",
			suffix:   ".conf",
			uid:      0,
			want:     []string{"/usr/share/containers/storage.rootful.conf.d", "/usr/share/containers/storage.conf.d"},
		},
		{
			name:     "storage.conf rootless",
			mainPath: "/usr/share/containers/storage.conf",
			suffix:   ".conf",
			uid:      1000,
			want:     []string{"/usr/share/containers/storage.rootless.conf.d/1000", "/usr/share/containers/storage.rootless.conf.d", "/usr/share/containers/storage.conf.d"},
		},
		{
			name:     "registries.d",
			mainPath: "/usr/share/containers/registries",
			suffix:   ".yaml",
			uid:      0,
			want:     []string{"/usr/share/containers/registries.rootful.d", "/usr/share/containers/registries.d"},
		},
		{
			name:     "registries.d rootless",
			mainPath: "/usr/share/containers/registries",
			suffix:   ".yaml",
			uid:      99,
			want:     []string{"/usr/share/containers/registries.rootless.d/99", "/usr/share/containers/registries.rootless.d", "/usr/share/containers/registries.d"},
		},
		{
			name:     "uid -1 (like on windows)",
			mainPath: "/etc/containers/containers.conf",
			suffix:   ".conf",
			uid:      -1,
			want:     []string{"/etc/containers/containers.conf.d"},
		},
	}
	t.Parallel()
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := getDropInPathsUnderMain(tt.mainPath, tt.suffix, tt.uid)
			assert.Equal(t, tt.want, got)
		})
	}
}

// File layout of the test files, map key is filename/path and value is the content
type testfiles struct {
	usr  map[string]string
	etc  map[string]string
	home map[string]string
}

func Test_Read(t *testing.T) {
	type testcase struct {
		name string
		// Arguments for this function
		arg File
		// Layout of the actual files we try to parse
		files testfiles
		// setup is extra setup that must be run before the test
		setup func(t *testing.T, tc *testcase)
		// Expected result, file content in right order.
		want []string
		// wantErr is matched with errors.Is() when the function should error instead.
		wantErr error
		// wantErrContains, if non-empty, asserts a substring of the error string instead of wantErr.
		wantErrContains string
	}

	tests := []testcase{
		{
			name: "no files",
			arg: File{
				Name:      "containers",
				Extension: "conf",
			},
			want: nil,
		},
		{
			name: "no files error if not found",
			arg: File{
				Name:            "containers",
				Extension:       "conf",
				ErrorIfNotFound: true,
			},
			// Read records real paths (under RootForImplicitAbsolutePaths / XDG); the message is fmt.Errorf(..., %q, usedPaths).
			wantErrContains: "no containers.conf file found; searched paths:",
			wantErr:         ErrConfigFileNotFound,
		},
		{
			name: "simple main file",
			arg: File{
				Name:      "containers",
				Extension: "conf",
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf": "content1",
					// file with different name should not be read
					"storage.conf": "content2",
				},
			},
			want: []string{"content1"},
		},
		{
			name: "etc overrides usr file",
			arg: File{
				Name:      "containers",
				Extension: "conf",
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf": "content1",
				},
				etc: map[string]string{
					"containers.conf": "file2",
				},
			},
			want: []string{"file2"},
		},
		{
			name: "home overrides etc and usr file",
			arg: File{
				Name:      "containers",
				Extension: "conf",
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf": "content1",
				},
				etc: map[string]string{
					"containers.conf": "file2",
				},
				home: map[string]string{
					"containers.conf": "home",
				},
			},
			want: []string{"home"},
		},
		{
			name: "single drop in",
			arg: File{
				Name:      "containers",
				Extension: "conf",
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf.d/10-myconf.conf": "content1",
				},
			},
			want: []string{"content1"},
		},
		{
			name: "drop in and main file",
			arg: File{
				Name:      "containers",
				Extension: "conf",
			},

			files: testfiles{
				usr: map[string]string{
					"containers.conf":                  "file1",
					"containers.conf.d/10-myconf.conf": "file2",
				},
			},
			want: []string{"file1", "file2"},
		},
		{
			name: "drop in and main file on different paths",
			arg: File{
				Name:      "containers",
				Extension: "conf",
			},

			files: testfiles{
				usr: map[string]string{
					"containers.conf.d/10-myconf.conf": "usr",
				},
				etc: map[string]string{
					"containers.conf": "etc",
				},
			},
			want: []string{"etc", "usr"},
		},
		{
			name: "drop in order",
			arg: File{
				Name:      "containers",
				Extension: "conf",
			},

			files: testfiles{
				usr: map[string]string{
					"containers.conf.d/20-conf2.conf": "2",
					"containers.conf.d/40-conf4.conf": "4",
				},
				etc: map[string]string{
					"containers.conf.d/10-conf1.conf": "1",
				},
				home: map[string]string{
					"containers.conf.d/30-conf3.conf": "3",
				},
			},
			want: []string{"1", "2", "3", "4"},
		},
		{
			name: "drop in override",
			arg: File{
				Name:      "containers",
				Extension: "conf",
			},
			files: testfiles{
				usr: map[string]string{
					// This should be ignored because etc has the same filename
					"containers.conf.d/10-settings.conf": "usr-content",
					"containers.conf.d/20-settings.conf": "usr-content-2",
				},
				etc: map[string]string{
					// This should win
					"containers.conf.d/10-settings.conf": "etc-override",
				},
			},
			want: []string{"etc-override", "usr-content-2"},
		},
		{
			name: "drop in ignores wrong extensions",
			arg: File{
				Name:      "containers",
				Extension: "conf",
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf.d/10-valid.conf": "valid",
					"containers.conf.d/README.md":     "ignore-me",
					"containers.conf.d/backup.conf~":  "ignore-me-too",
				},
			},
			want: []string{"valid"},
		},
		{
			name: "drop in supports symlink",
			arg: File{
				Name:      "containers",
				Extension: "conf",
			},
			setup: func(t *testing.T, tc *testcase) {
				realFile := filepath.Join(t.TempDir(), "real.conf")
				require.NoError(t, os.WriteFile(realFile, []byte("symlinked"), 0o600))

				dropInDir := filepath.Join(tc.arg.RootForImplicitAbsolutePaths, systemConfigPath, "containers.conf.d")
				require.NoError(t, os.MkdirAll(dropInDir, 0o755))
				require.NoError(t, os.Symlink(realFile, filepath.Join(dropInDir, "10-symlink.conf")))
			},
			want: []string{"symlinked"},
		},
		{
			name: "policy.json main files only (ignore drop-ins)",
			arg: File{
				Name:                 "policy",
				Extension:            "json",
				DoNotLoadDropInFiles: true,
			},
			files: testfiles{
				usr: map[string]string{
					"policy.json":                 "main",
					"policy.json.d/10-extra.json": "drop-in",
				},
			},
			want: []string{"main"},
		},
		{
			name: "registries.d drop ins only (ignore main)",
			arg: File{
				Name:                           "registries",
				Extension:                      "yaml",
				DoNotLoadMainFiles:             true,
				DoNotUseExtensionForConfigName: true,
			},
			files: testfiles{
				usr: map[string]string{
					"registries.yaml":            "main",
					"registries.d/10-extra.yaml": "drop-in",
				},
			},
			want: []string{"drop-in"},
		},
		{
			name: "rootless specific drop-ins",
			arg: File{
				Name:      "containers",
				Extension: "conf",
				UserId:    1000,
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf.d/01-global.conf":        "global",
					"containers.rootless.conf.d/02-user.conf": "rootless-specific",
					"containers.rootful.conf.d/02-root.conf":  "rootful-ignored",
				},
			},
			want: []string{"global", "rootless-specific"},
		},
		{
			name: "rootless uid specific drop-ins",
			arg: File{
				Name:      "containers",
				Extension: "conf",
				UserId:    1000,
			},
			files: testfiles{
				usr: map[string]string{
					"containers.rootless.conf.d/1000/settings.conf": "uid-1000",
					"containers.rootless.conf.d/99/settings.conf":   "uid-99",
				},
			},
			want: []string{"uid-1000"},
		},
		{
			name: "containers.conf env var not being set",
			arg: File{
				Name:            "containers",
				Extension:       "conf",
				EnvironmentName: "CONTAINERS_CONF",
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf": "content1",
				},
			},
			want: []string{"content1"},
		},
		{
			name: "containers.conf env var must override all files",
			arg: File{
				Name:            "containers",
				Extension:       "conf",
				EnvironmentName: "CONTAINERS_CONF",
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf":           "content1",
					"containers.conf.d/01.conf": "01",
				},
			},
			setup: func(t *testing.T, tc *testcase) {
				// filename does not need to end in .conf
				file := filepath.Join(t.TempDir(), "somepath")
				err := os.WriteFile(file, []byte("env"), 0o600)
				require.NoError(t, err)
				t.Setenv("CONTAINERS_CONF", file)
			},
			want: []string{"env"},
		},
		{
			name: "containers.conf override env var should be appended",
			arg: File{
				Name:            "containers",
				Extension:       "conf",
				EnvironmentName: "CONTAINERS_CONF",
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf":           "content1",
					"containers.conf.d/01.conf": "01",
				},
			},
			setup: func(t *testing.T, tc *testcase) {
				file := filepath.Join(t.TempDir(), "somepath")
				err := os.WriteFile(file, []byte("env"), 0o600)
				require.NoError(t, err)
				t.Setenv("CONTAINERS_CONF_OVERRIDE", file)
			},
			want: []string{"content1", "01", "env"},
		},
		{
			name: "containers.conf both env var should be appended",
			arg: File{
				Name:            "containers",
				Extension:       "conf",
				EnvironmentName: "CONTAINERS_CONF",
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf":           "content1",
					"containers.conf.d/01.conf": "01",
				},
			},
			setup: func(t *testing.T, tc *testcase) {
				file1 := filepath.Join(t.TempDir(), "path1")
				err := os.WriteFile(file1, []byte("env1"), 0o600)
				require.NoError(t, err)
				t.Setenv("CONTAINERS_CONF", file1)

				file2 := filepath.Join(t.TempDir(), "path1")
				err = os.WriteFile(file2, []byte("env2"), 0o600)
				require.NoError(t, err)
				t.Setenv("CONTAINERS_CONF_OVERRIDE", file2)
			},
			want: []string{"env1", "env2"},
		},
		{
			name: "env var should error on non existing file",
			arg: File{
				Name:            "containers",
				Extension:       "conf",
				EnvironmentName: "CONTAINERS_CONF",
			},
			setup: func(t *testing.T, tc *testcase) {
				file := filepath.Join(t.TempDir(), "123")
				t.Setenv("CONTAINERS_CONF", file)
			},
			wantErr: fs.ErrNotExist,
		},
		{
			name: "override env var should error on non existing file",
			arg: File{
				Name:            "containers",
				Extension:       "conf",
				EnvironmentName: "CONTAINERS_CONF",
			},
			setup: func(t *testing.T, tc *testcase) {
				file := filepath.Join(t.TempDir(), "123")
				t.Setenv("CONTAINERS_CONF_OVERRIDE", file)
			},
			wantErr: fs.ErrNotExist,
		},
		{
			name: "containers.conf with modules",
			arg: File{
				Name:            "containers",
				Extension:       "conf",
				EnvironmentName: "CONTAINERS_CONF",
				Modules:         []string{"module.abc"},
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf": "content1",
				},
				home: map[string]string{
					// file extension should not matter for modules
					"containers.conf.modules/module.abc": "relative module",
				},
			},
			setup: func(t *testing.T, tc *testcase) {
				file := filepath.Join(t.TempDir(), "somepath")
				err := os.WriteFile(file, []byte("absolute module"), 0o600)
				require.NoError(t, err)
				tc.arg.Modules = append(tc.arg.Modules, file)
			},
			want: []string{"content1", "relative module", "absolute module"},
		},
		{
			name: "containers.conf with module override",
			arg: File{
				Name:            "containers",
				Extension:       "conf",
				EnvironmentName: "CONTAINERS_CONF",
				Modules:         []string{"module.conf", "different.conf"},
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf.modules/module.conf": "usr",
				},
				etc: map[string]string{
					"containers.conf.modules/different.conf": "etc",
				},
				home: map[string]string{
					"containers.conf.modules/module.conf": "home",
				},
			},
			want: []string{"home", "etc"},
		},
		{
			// same as above except we switch the module order to ensure we read the files in the proper order as given
			name: "containers.conf with module override inverse module order",
			arg: File{
				Name:            "containers",
				Extension:       "conf",
				EnvironmentName: "CONTAINERS_CONF",
				Modules:         []string{"different.conf", "module.conf"},
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf.modules/module.conf": "usr",
				},
				etc: map[string]string{
					"containers.conf.modules/different.conf": "etc",
				},
				home: map[string]string{
					"containers.conf.modules/module.conf": "home",
				},
			},
			want: []string{"etc", "home"},
		},
		{
			name: "containers.conf env and modules order",
			arg: File{
				Name:            "containers",
				Extension:       "conf",
				EnvironmentName: "CONTAINERS_CONF",
				Modules:         []string{"module.conf"},
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf":                     "content1",
					"containers.conf.d/01.conf":           "01",
					"containers.conf.modules/module.conf": "mod",
				},
			},

			setup: func(t *testing.T, tc *testcase) {
				file1 := filepath.Join(t.TempDir(), "path1")
				err := os.WriteFile(file1, []byte("env1"), 0o600)
				require.NoError(t, err)
				t.Setenv("CONTAINERS_CONF", file1)

				file2 := filepath.Join(t.TempDir(), "path1")
				err = os.WriteFile(file2, []byte("env2"), 0o600)
				require.NoError(t, err)
				t.Setenv("CONTAINERS_CONF_OVERRIDE", file2)
			},
			// CONTAINERS_CONF, then modules, then CONTAINERS_CONF_OVERRIDE
			want: []string{"env1", "mod", "env2"},
		},
		{
			name: "CustomConfigFilePath with drop in",
			arg: File{
				Name:      "containers",
				Extension: "conf",
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf":                  "main",
					"containers.conf.d/10-myconf.conf": "drop in",
				},
			},
			setup: func(t *testing.T, tc *testcase) {
				file := filepath.Join(t.TempDir(), "somepath")
				err := os.WriteFile(file, []byte("custom"), 0o600)
				require.NoError(t, err)

				tc.arg.CustomConfigFilePath = file
			},
			want: []string{"custom", "drop in"},
		},
		{
			name: "CustomConfigFilePath ENOENT",
			arg: File{
				Name:      "containers",
				Extension: "conf",
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf":                  "main",
					"containers.conf.d/10-myconf.conf": "drop in",
				},
			},
			setup: func(t *testing.T, tc *testcase) {
				file := filepath.Join(t.TempDir(), "IDoNotExist")

				tc.arg.CustomConfigFilePath = file
			},
			wantErr: fs.ErrNotExist,
		},
		{
			name: "CustomConfigFilePath must win over env",
			arg: File{
				Name:            "containers",
				Extension:       "conf",
				EnvironmentName: "CONTAINERS_CONF",
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf":                  "main",
					"containers.conf.d/10-myconf.conf": "drop in",
				},
			},
			setup: func(t *testing.T, tc *testcase) {
				file := filepath.Join(t.TempDir(), "file")
				err := os.WriteFile(file, []byte("explicit path"), 0o600)
				require.NoError(t, err)
				tc.arg.CustomConfigFilePath = file

				file1 := filepath.Join(t.TempDir(), "path1")
				err = os.WriteFile(file1, []byte("env"), 0o600)
				require.NoError(t, err)
				t.Setenv("CONTAINERS_CONF", file1)
			},
			want: []string{"explicit path", "drop in"},
		},
		{
			name: "CustomConfigFileDropInDirectory with main file",
			arg: File{
				Name:      "containers",
				Extension: "conf",
			},

			files: testfiles{
				usr: map[string]string{
					"containers.conf":                  "main",
					"containers.conf.d/10-myconf.conf": "drop in",
				},
			},
			setup: func(t *testing.T, tc *testcase) {
				dir := t.TempDir()
				err := os.WriteFile(filepath.Join(dir, "file.conf"), []byte("custom"), 0o600)
				require.NoError(t, err)

				// write a second file without .conf which should not be parsed
				err = os.WriteFile(filepath.Join(dir, "somefile"), []byte("somefile"), 0o600)
				require.NoError(t, err)

				tc.arg.CustomConfigFileDropInDirectory = dir
			},
			want: []string{"main", "custom"},
		},
		{
			name: "CustomConfigFileDropInDirectory does not error with ENOENT",
			arg: File{
				Name:      "containers",
				Extension: "conf",
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf":                  "main",
					"containers.conf.d/10-myconf.conf": "drop in",
				},
			},
			setup: func(t *testing.T, tc *testcase) {
				dir := filepath.Join(t.TempDir(), "dirDoesNotExist")
				tc.arg.CustomConfigFileDropInDirectory = dir
			},
			want: []string{"main"},
		},
		{
			name: "CustomConfigFileDropInDirectory must win over env",
			arg: File{
				Name:            "containers",
				Extension:       "conf",
				EnvironmentName: "CONTAINERS_CONF",
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf":                  "main",
					"containers.conf.d/10-myconf.conf": "drop in",
				},
			},
			setup: func(t *testing.T, tc *testcase) {
				dir := t.TempDir()
				err := os.WriteFile(filepath.Join(dir, "file.conf"), []byte("explicit dir"), 0o600)
				require.NoError(t, err)

				tc.arg.CustomConfigFileDropInDirectory = dir

				file2 := filepath.Join(t.TempDir(), "path1")
				err = os.WriteFile(file2, []byte("env"), 0o600)
				require.NoError(t, err)
				t.Setenv("CONTAINERS_CONF_OVERRIDE", file2)
			},
			want: []string{"main", "explicit dir"},
		},
		{
			name: "CustomConfigFilePath and CustomConfigFileDropInDirectory",
			arg: File{
				Name:      "containers",
				Extension: "conf",
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf":                  "main",
					"containers.conf.d/10-myconf.conf": "drop in",
				},
			},
			setup: func(t *testing.T, tc *testcase) {
				file := filepath.Join(t.TempDir(), "file")
				err := os.WriteFile(file, []byte("custom main"), 0o600)
				require.NoError(t, err)
				tc.arg.CustomConfigFilePath = file

				dir := t.TempDir()
				err = os.WriteFile(filepath.Join(dir, "file.conf"), []byte("custom dir"), 0o600)
				require.NoError(t, err)

				tc.arg.CustomConfigFileDropInDirectory = dir
			},
			want: []string{"custom main", "custom dir"},
		},
		{
			name: "CustomConfigFilePath and CustomConfigFileDropInDirectory must win over envs",
			arg: File{
				Name:            "containers",
				Extension:       "conf",
				EnvironmentName: "CONTAINERS_CONF",
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf":                  "main",
					"containers.conf.d/10-myconf.conf": "drop in",
				},
			},
			setup: func(t *testing.T, tc *testcase) {
				file := filepath.Join(t.TempDir(), "file")
				err := os.WriteFile(file, []byte("explicit path"), 0o600)
				require.NoError(t, err)
				tc.arg.CustomConfigFilePath = file

				file1 := filepath.Join(t.TempDir(), "path1")
				err = os.WriteFile(file1, []byte("main env"), 0o600)
				require.NoError(t, err)
				t.Setenv("CONTAINERS_CONF", file1)

				dir := t.TempDir()
				err = os.WriteFile(filepath.Join(dir, "file.conf"), []byte("explicit dir"), 0o600)
				require.NoError(t, err)

				tc.arg.CustomConfigFileDropInDirectory = dir

				file2 := filepath.Join(t.TempDir(), "path1")
				err = os.WriteFile(file2, []byte("override env"), 0o600)
				require.NoError(t, err)
				t.Setenv("CONTAINERS_CONF_OVERRIDE", file2)
			},
			want: []string{"explicit path", "explicit dir"},
		},
		{
			name: "windows with user id -1",
			arg: File{
				Name:      "containers",
				Extension: "conf",
				UserId:    -1,
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf.d/true-1.conf":        "true-1",
					"containers.rootless.conf.d/-1/a.conf": "a",
					"containers.rootless.conf.d/b.conf":    "b",
					"containers.rootful.conf.d/x.conf":     "x",
				},
				etc: map[string]string{
					"containers.conf.d/true-2.conf":        "true-2",
					"containers.rootless.conf.d/-1/c.conf": "c",
					"containers.rootless.conf.d/d.conf":    "d",
					"containers.rootful.conf.d/z.conf":     "z",
				},
			},
			// Special drops in must be ignored for uid < 0,
			// it only reads the main .conf.d locations.
			want: []string{"true-1", "true-2"},
		},
		{
			name: "rootless drop-in order",
			arg: File{
				Name:      "containers",
				Extension: "conf",
				UserId:    10,
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf.d/b.conf":             "b1",
					"containers.rootless.conf.d/b.conf":    "b2",
					"containers.rootless.conf.d/10/b.conf": "b3",
				},
				etc: map[string]string{
					"containers.conf.d/a.conf":             "a1",
					"containers.rootless.conf.d/a.conf":    "a2",
					"containers.rootless.conf.d/10/a.conf": "a3",
				},
			},
			want: []string{"a3", "b3"},
		},
		{
			name: "rootful drop-in order",
			arg: File{
				Name:      "containers",
				Extension: "conf",
				UserId:    0,
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf.d/b.conf":         "b1",
					"containers.rootful.conf.d/b.conf": "b2",
				},
				etc: map[string]string{
					"containers.conf.d/a.conf":         "a1",
					"containers.rootful.conf.d/a.conf": "a2",
				},
			},
			want: []string{"a2", "b2"},
		},
		{
			name: "drop-in order etc overwrites specific usr path",
			arg: File{
				Name:      "containers",
				Extension: "conf",
				UserId:    10,
			},
			files: testfiles{
				usr: map[string]string{
					"containers.rootless.conf.d/10/a.conf": "usr",
				},
				etc: map[string]string{
					"containers.conf.d/a.conf": "etc",
				},
			},
			want: []string{"etc"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.arg.RootForImplicitAbsolutePaths = t.TempDir()
			writeTestFiles(t, tt.arg.RootForImplicitAbsolutePaths, tt.files)
			if tt.setup != nil {
				tt.setup(t, &tt)
			}
			seq := Read(&tt.arg)
			if tt.wantErr == nil && tt.wantErrContains == "" {
				confs := collectConfigs(t, seq)
				assert.Equal(t, tt.want, confs)

				// ensure the modules all get resolves to absolute paths and are valid
				for _, module := range tt.arg.Modules {
					assert.FileExists(t, module)
					assert.True(t, filepath.IsAbs(module))
				}
			} else {
				next, stop := iter.Pull2(seq)
				defer stop()

				_, err, ok := next()
				assert.True(t, ok)
				if tt.wantErrContains != "" {
					assert.ErrorContains(t, err, tt.wantErrContains)
					assert.Contains(t, err.Error(), tt.arg.RootForImplicitAbsolutePaths)
				}
				if tt.wantErr != nil {
					assert.ErrorIs(t, err, tt.wantErr)
				}

				// end of iterator
				_, _, ok = next()
				assert.False(t, ok)
			}
		})
	}
}

func writeTestFiles(t *testing.T, tmpdir string, files testfiles) {
	t.Helper()
	usr := filepath.Join(tmpdir, systemConfigPath)
	require.NoError(t, os.MkdirAll(usr, 0o755))
	writeTestFileMap(t, usr, files.usr)

	etc := filepath.Join(tmpdir, adminOverrideConfigPath)
	require.NoError(t, os.MkdirAll(etc, 0o755))
	writeTestFileMap(t, etc, files.etc)

	home := filepath.Join(tmpdir, "home")
	t.Setenv("XDG_CONFIG_HOME", home)
	homeContainers := filepath.Join(home, "containers")
	require.NoError(t, os.MkdirAll(homeContainers, 0o755))
	writeTestFileMap(t, homeContainers, files.home)
}

func writeTestFileMap(t *testing.T, path string, files map[string]string) {
	t.Helper()
	for name, value := range files {
		fullPath := filepath.Join(path, name)
		require.NoError(t, os.MkdirAll(filepath.Dir(fullPath), 0o755))
		err := os.WriteFile(fullPath, []byte(value), 0o600)
		require.NoError(t, err)
	}
}

// collectConfigs consumes the iterator and returns the content of the files read
func collectConfigs(t *testing.T, seq iter.Seq2[*Item, error]) []string {
	var contents []string
	for item, err := range seq {
		require.NoError(t, err)
		require.NotNil(t, item)
		data, err := io.ReadAll(item.Reader)
		require.NoError(t, err)

		contents = append(contents, string(data))
	}
	return contents
}

func Test_ParseTOML(t *testing.T) {
	type Config struct {
		Field1 bool
		Field2 string
		Field3 int
	}

	tests := []struct {
		name string
		// Arguments for this function
		arg File
		// Layout of the actual files we try to parse
		files testfiles
		// Expected result
		want *Config
		// wantErr set to the expected error message
		wantErr string
	}{
		{
			name: "simple parse",
			arg: File{
				Name:      "containers",
				Extension: "conf",
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf": "field1 = true\n",
				},
			},
			want: &Config{
				Field1: true,
			},
		},
		{
			name: "drop in parse",
			arg: File{
				Name:      "containers",
				Extension: "conf",
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf":           "field1 = true\n",
					"containers.conf.d/10.conf": "field2 = \"abc\"",
				},
			},
			want: &Config{
				Field1: true,
				Field2: "abc",
			},
		},
		{
			name: "main file override",
			arg: File{
				Name:      "containers",
				Extension: "conf",
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf":           "field1 = true\n",
					"containers.conf.d/10.conf": "field2 = \"abc\"",
				},
				etc: map[string]string{
					"containers.conf": "field3 = 1\n",
				},
			},
			want: &Config{
				Field2: "abc",
				Field3: 1,
			},
		},
		{
			name: "invalid toml",
			arg: File{
				Name:      "containers",
				Extension: "conf",
			},
			files: testfiles{
				usr: map[string]string{
					"containers.conf": "blah\n",
				},
			},
			wantErr: "toml: line 1: expected '.' or '='",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.arg.RootForImplicitAbsolutePaths = t.TempDir()
			writeTestFiles(t, tt.arg.RootForImplicitAbsolutePaths, tt.files)

			conf := new(Config)
			err := ParseTOML(conf, &tt.arg)
			if tt.wantErr == "" {
				require.NoError(t, err)
				assert.Equal(t, tt.want, conf)
			} else {
				assert.ErrorContains(t, err, tt.wantErr)
			}
		})
	}
}

func TestGetSearchPaths(t *testing.T) {
	t.Setenv("XDG_CONFIG_HOME", "/home")
	tests := []struct {
		name string
		conf File
		want SearchPaths
	}{
		{
			name: "basic containers.conf",
			conf: File{
				Name:      "containers",
				Extension: "conf",
			},
			want: SearchPaths{
				MainFiles: []string{
					"/home/containers/containers.conf",
					adminOverrideConfigPath + "/containers.conf",
					systemConfigPath + "/containers.conf",
				},
				DropInDirectories: []string{
					"/home/containers/containers.conf.d",
					adminOverrideConfigPath + "/containers.rootful.conf.d",
					adminOverrideConfigPath + "/containers.conf.d",
					systemConfigPath + "/containers.rootful.conf.d",
					systemConfigPath + "/containers.conf.d",
				},
			},
		},
		{
			name: "basic policy.json",
			conf: File{
				Name:                 "policy",
				Extension:            "json",
				DoNotLoadDropInFiles: true,
			},
			want: SearchPaths{
				MainFiles: []string{
					"/home/containers/policy.json",
					adminOverrideConfigPath + "/policy.json",
					systemConfigPath + "/policy.json",
				},
			},
		},
		// TODO: add more test cases
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := GetSearchPaths(&tt.conf)
			require.NoError(t, err)
			assert.Equal(t, tt.want, got)
		})
	}
}
