package cmd

import (
	"fmt"
	"io/fs"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"strings"
	"sync/atomic"
	"testing"

	"github.com/alecthomas/assert/v2"
	"github.com/twpayne/go-vfs/v5"
	"github.com/twpayne/go-vfs/v5/vfst"

	"chezmoi.io/chezmoi/v2/internal/chezmoitest"
)

func TestApplyCmd(t *testing.T) {
	for _, tc := range []struct {
		name      string
		extraRoot any
		args      []string
		tests     []any
	}{
		{
			name: "all",
			tests: []any{
				vfst.TestPath("/home/user/.create",
					vfst.TestModeIsRegular(),
					vfst.TestModePerm(0o666&^chezmoitest.Umask),
					vfst.TestContentsString("# contents of .create\n"),
				),
				vfst.TestPath("/home/user/.dir",
					vfst.TestIsDir(),
					vfst.TestModePerm(fs.ModePerm&^chezmoitest.Umask),
				),
				vfst.TestPath("/home/user/.dir/file",
					vfst.TestModeIsRegular(),
					vfst.TestModePerm(0o666&^chezmoitest.Umask),
					vfst.TestContentsString("# contents of .dir/file\n"),
				),
				vfst.TestPath("/home/user/.dir/subdir",
					vfst.TestIsDir(),
					vfst.TestModePerm(fs.ModePerm&^chezmoitest.Umask),
				),
				vfst.TestPath("/home/user/.dir/subdir/file",
					vfst.TestModeIsRegular(),
					vfst.TestModePerm(0o666&^chezmoitest.Umask),
					vfst.TestContentsString("# contents of .dir/subdir/file\n"),
				),
				vfst.TestPath("/home/user/.empty",
					vfst.TestModeIsRegular(),
					vfst.TestModePerm(0o666&^chezmoitest.Umask),
					vfst.TestContents(nil),
				),
				vfst.TestPath("/home/user/.executable",
					vfst.TestModeIsRegular(),
					vfst.TestModePerm(fs.ModePerm&^chezmoitest.Umask),
					vfst.TestContentsString("# contents of .executable\n"),
				),
				vfst.TestPath("/home/user/.file",
					vfst.TestModeIsRegular(),
					vfst.TestModePerm(0o666&^chezmoitest.Umask),
					vfst.TestContentsString("# contents of .file\n"),
				),
				vfst.TestPath("/home/user/.private",
					vfst.TestModeIsRegular(),
					vfst.TestModePerm(0o600&^chezmoitest.Umask),
					vfst.TestContentsString("# contents of .private\n"),
				),
				vfst.TestPath("/home/user/.remove",
					vfst.TestDoesNotExist(),
				),
				vfst.TestPath("/home/user/.symlink",
					vfst.TestModeType(fs.ModeSymlink),
					vfst.TestSymlinkTarget(filepath.FromSlash(".dir/subdir/file")),
				),
				vfst.TestPath("/home/user/.template",
					vfst.TestModeIsRegular(),
					vfst.TestModePerm(0o666&^chezmoitest.Umask),
					vfst.TestContentsString("key = value\n"),
				),
			},
		},
		{
			name: "all_with_--dry-run",
			args: []string{"--dry-run"},
			tests: []any{
				vfst.TestPath("/home/user/.create",
					vfst.TestDoesNotExist(),
				),
				vfst.TestPath("/home/user/.dir",
					vfst.TestDoesNotExist(),
				),
				vfst.TestPath("/home/user/.empty",
					vfst.TestDoesNotExist(),
				),
				vfst.TestPath("/home/user/.executable",
					vfst.TestDoesNotExist(),
				),
				vfst.TestPath("/home/user/.file",
					vfst.TestDoesNotExist(),
				),
				vfst.TestPath("/home/user/.private",
					vfst.TestDoesNotExist(),
				),
				vfst.TestPath("/home/user/.remove",
					vfst.TestDoesNotExist(),
				),
				vfst.TestPath("/home/user/.symlink",
					vfst.TestDoesNotExist(),
				),
				vfst.TestPath("/home/user/.template",
					vfst.TestDoesNotExist(),
				),
			},
		},
		{
			name: "dir",
			args: []string{"~/.dir"},
			tests: []any{
				vfst.TestPath("/home/user/.dir",
					vfst.TestIsDir(),
					vfst.TestModePerm(fs.ModePerm&^chezmoitest.Umask),
				),
				vfst.TestPath("/home/user/.dir/file",
					vfst.TestModeIsRegular(),
					vfst.TestModePerm(0o666&^chezmoitest.Umask),
					vfst.TestContentsString("# contents of .dir/file\n"),
				),
				vfst.TestPath("/home/user/.dir/subdir",
					vfst.TestIsDir(),
					vfst.TestModePerm(fs.ModePerm&^chezmoitest.Umask),
				),
				vfst.TestPath("/home/user/.dir/subdir/file",
					vfst.TestModeIsRegular(),
					vfst.TestModePerm(0o666&^chezmoitest.Umask),
					vfst.TestContentsString("# contents of .dir/subdir/file\n"),
				),
			},
		},
		{
			name: "dir_with_--recursive=false",
			args: []string{"~/.dir", "--recursive=false"},
			tests: []any{
				vfst.TestPath("/home/user/.dir",
					vfst.TestIsDir(),
					vfst.TestModePerm(fs.ModePerm&^chezmoitest.Umask),
				),
				vfst.TestPath("/home/user/.dir/file",
					vfst.TestDoesNotExist(),
				),
				vfst.TestPath("/home/user/.dir/subdir",
					vfst.TestDoesNotExist(),
				),
			},
		},
		{
			name: "create",
			args: []string{"~/.create"},
			extraRoot: map[string]any{
				"/home/user/.create": "# existing contents of .create\n",
			},
			tests: []any{
				vfst.TestPath("/home/user/.create",
					vfst.TestModeIsRegular(),
					vfst.TestModePerm(0o666&^chezmoitest.Umask),
					vfst.TestContentsString("# existing contents of .create\n"),
				),
			},
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			chezmoitest.WithTestFS(t, map[string]any{
				"/home/user": map[string]any{
					".config": map[string]any{
						"chezmoi": map[string]any{
							"chezmoi.toml": chezmoitest.JoinLines(
								`[data]`,
								`  variable = "value"`,
							),
						},
					},
					".local": map[string]any{
						"share": map[string]any{
							"chezmoi": map[string]any{
								"create_dot_create": "# contents of .create\n",
								"dot_dir": map[string]any{
									"file": "# contents of .dir/file\n",
									"subdir": map[string]any{
										"file": "# contents of .dir/subdir/file\n",
									},
								},
								"dot_file":   "# contents of .file\n",
								"dot_remove": "",
								"dot_template.tmpl": chezmoitest.JoinLines(
									`key = {{ "value" }}`,
								),
								"empty_dot_empty":           "",
								"executable_dot_executable": "# contents of .executable\n",
								"private_dot_private":       "# contents of .private\n",
								"symlink_dot_symlink":       ".dir/subdir/file\n",
							},
						},
					},
				},
			}, func(fileSystem vfs.FS) {
				if tc.extraRoot != nil {
					assert.NoError(t, vfst.NewBuilder().Build(fileSystem, tc.extraRoot))
				}
				assert.NoError(t, newTestConfig(t, fileSystem).execute(append([]string{"apply"}, tc.args...)))
				vfst.RunTests(t, fileSystem, "", tc.tests)
			})
		})
	}
}

func TestApplySkipSecrets(t *testing.T) {
	chezmoitest.WithTestFS(t, map[string]any{
		"/home/user": map[string]any{
			".local/share/chezmoi": map[string]any{
				"dot_file":           "# contents of .file\n",
				"dot_secret.tmpl":    `{{ secret "password" }}`,
				"dot_encrypted.tmpl": `{{ decrypt "ciphertext" }}`,
			},
		},
	}, func(fileSystem vfs.FS) {
		c := newTestConfig(t, fileSystem)
		c.skipSecrets = true
		assert.NoError(t, c.execute([]string{"apply"}))
		vfst.RunTests(t, fileSystem, "",
			vfst.TestPath("/home/user/.encrypted",
				vfst.TestDoesNotExist(),
			),
			vfst.TestPath("/home/user/.file",
				vfst.TestContentsString("# contents of .file\n"),
			),
			vfst.TestPath("/home/user/.secret",
				vfst.TestDoesNotExist(),
			),
		)
	})
}

func TestUserAgent(t *testing.T) {
	var userAgent string
	httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		userAgent = r.Header.Get("User-Agent")
	}))
	defer httpServer.Close()

	chezmoitest.WithTestFS(t, map[string]any{
		"/home/user": map[string]any{
			".local/share/chezmoi": map[string]any{
				".chezmoiexternal.toml.tmpl": chezmoitest.JoinLines(
					`[".local/bin/file"]`,
					`    type = "file"`,
					`    url = "`+httpServer.URL+`/file"`,
				),
			},
		},
	}, func(fileSystem vfs.FS) {
		assert.NoError(t, newTestConfig(t, fileSystem).execute([]string{"apply"}))
	})

	assert.True(t, strings.HasPrefix(userAgent, "chezmoi.io/"))
}

func TestIssue2132(t *testing.T) {
	chezmoitest.WithTestFS(t, map[string]any{
		"/home/user/.local/share/chezmoi/remove_dot_dir/non_existent_file": "",
	}, func(fileSystem vfs.FS) {
		config1 := newTestConfig(t, fileSystem)
		assert.NoError(t, config1.execute([]string{"apply"}))
		vfst.RunTests(t, fileSystem, "",
			vfst.TestPath("/home/user/.dir",
				vfst.TestDoesNotExist(),
			),
		)
		config2 := newTestConfig(t, fileSystem)
		assert.NoError(t, config2.execute([]string{"apply", "--no-tty"}))
		vfst.RunTests(t, fileSystem, "",
			vfst.TestPath("/home/user/.dir",
				vfst.TestDoesNotExist(),
			),
		)
	})
}

func TestIssue3206(t *testing.T) {
	chezmoitest.WithTestFS(t, map[string]any{
		"/home/user": map[string]any{
			".local/share/chezmoi": map[string]any{
				".chezmoiignore": "",
				"dot_config/private_expanso/match": map[string]any{
					".chezmoidata.yaml": "key: value\n",
					"greek.yml.tmpl":    "{{ .key }}",
				},
			},
		},
	}, func(fileSystem vfs.FS) {
		assert.NoError(t, newTestConfig(t, fileSystem).execute([]string{"apply"}))
	})
}

func TestIssue3216(t *testing.T) {
	chezmoitest.WithTestFS(t, map[string]any{
		"/home/user": map[string]any{
			".local/share/chezmoi": map[string]any{
				".chezmoiignore": "",
				"dot_config/private_expanso/match": map[string]any{
					".chezmoidata.yaml": "",
					"greek.yml.tmpl":    "{{ .chezmoi.os }}",
				},
			},
		},
	}, func(fileSystem vfs.FS) {
		assert.NoError(t, newTestConfig(t, fileSystem).execute([]string{"apply"}))
	})
}

func TestIssue3703(t *testing.T) {
	httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, err := w.Write([]byte("contents of file\n"))
		assert.NoError(t, err)
	}))
	defer httpServer.Close()

	chezmoitest.WithTestFS(t, map[string]any{
		"/home/user": map[string]any{
			".local": map[string]any{
				"bin": map[string]any{
					"unmanaged": "",
				},
				"share/chezmoi": map[string]any{
					".chezmoiexternal.toml.tmpl": chezmoitest.JoinLines(
						`[".local/bin/file"]`,
						`    type = "file"`,
						`    url = "`+httpServer.URL+`/file"`,
					),
					"dot_local/exact_bin/.keep": "",
				},
			},
		},
	}, func(fileSystem vfs.FS) {
		assert.NoError(t, newTestConfig(t, fileSystem).execute([]string{"apply"}))
		vfst.RunTests(t, fileSystem, ".local/bin",
			vfst.TestPath("/home/user/.local/bin/file",
				vfst.TestContentsString("contents of file\n"),
			),
			vfst.TestPath("/home/user/.local/bin/unmanaged",
				vfst.TestDoesNotExist(),
			),
		)
	})
}

func TestIssue4927(t *testing.T) {
	// httpServer returns a different file contents every time.
	var count atomic.Int64
	httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, err := fmt.Fprintf(w, "contents of file, count %d\n", count.Add(1))
		assert.NoError(t, err)
	}))
	defer httpServer.Close()

	chezmoitest.WithTestFS(t, map[string]any{
		"/home/user": map[string]any{
			".local/share/chezmoi": map[string]any{
				".chezmoiexternal.toml.tmpl": chezmoitest.JoinLines(
					`[".file"]`,
					`    type = "file"`,
					`    url = "`+httpServer.URL+`/file"`,
					`    checksum.sha256 = "b48186336296270875649bac2e973968ec1f252154da32a4e906a462eb5f6c0a"`,
				),
			},
		},
	}, func(fileSystem vfs.FS) {
		// Apply an external.
		assert.NoError(t, newTestConfig(t, fileSystem).execute([]string{"apply"}))
		vfst.RunTests(t, fileSystem, ".file",
			vfst.TestPath("/home/user/.file",
				vfst.TestContentsString("contents of file, count 1\n"),
			),
		)

		// Clear the cache.
		assert.NoError(t, fileSystem.RemoveAll("/home/user/.cache/chezmoi"))

		// Apply the external again. As the cache has been cleared, apply should
		// download a new file with a new checksum.
		assert.NoError(t, fileSystem.WriteFile(
			"/home/user/.local/share/chezmoi/.chezmoiexternal.toml.tmpl",
			[]byte(chezmoitest.JoinLines(
				`[".file"]`,
				`    type = "file"`,
				`    url = "`+httpServer.URL+`/file"`,
				`    checksum.sha256 = "5ecfe762ad9a450cbc2317d8fd114d226ad97209ba1e11dc30ab26423a335fed"`,
			)),
			0o666))
		assert.NoError(t, newTestConfig(t, fileSystem).execute([]string{"apply"}))
		vfst.RunTests(t, fileSystem, ".file",
			vfst.TestPath("/home/user/.file",
				vfst.TestContentsString("contents of file, count 2\n"),
			),
		)

		// Clear the cache again.
		assert.NoError(t, fileSystem.RemoveAll("/home/user/.cache/chezmoi"))

		// Apply the external again. As the cache has been cleared, the apply
		// should download the external again, with a new SHA256 sum.
		assert.EqualError(
			t,
			newTestConfig(t, fileSystem).execute([]string{"apply"}),
			".file: .file: SHA256 mismatch: "+
				"expected 5ecfe762ad9a450cbc2317d8fd114d226ad97209ba1e11dc30ab26423a335fed, "+
				"got f1517c117381f6ac8c0b07e96152fe296891d9b56fb1ebc44ed0f8da2ab72688",
		)
		vfst.RunTests(t, fileSystem, ".file",
			vfst.TestPath("/home/user/.file",
				vfst.TestContentsString("contents of file, count 2\n"),
			),
		)

		// Apply the external again. As the cache has not be cleared, the apply
		// should have the same SHA256 sum as in the cache.
		assert.EqualError(t,
			newTestConfig(t, fileSystem).execute([]string{"apply"}),
			".file: .file: SHA256 mismatch: expected "+
				"5ecfe762ad9a450cbc2317d8fd114d226ad97209ba1e11dc30ab26423a335fed, "+
				"got f1517c117381f6ac8c0b07e96152fe296891d9b56fb1ebc44ed0f8da2ab72688",
		)
		vfst.RunTests(t, fileSystem, ".file",
			vfst.TestPath("/home/user/.file",
				vfst.TestContentsString("contents of file, count 2\n"),
			),
		)

		// Apply the external again, this time with --refresh-externals=always.
		// Due to the refresh, the SHA256 sum should have changed again.
		assert.EqualError(t,
			newTestConfig(t, fileSystem).execute([]string{"apply", "--refresh-externals=always"}),
			".file: .file: SHA256 mismatch: expected "+
				"5ecfe762ad9a450cbc2317d8fd114d226ad97209ba1e11dc30ab26423a335fed, "+
				"got 32da3f52b369ad56a60d0f9707268b5a8d0aba1839dde3fcfe6e3bf092777f97",
		)
		vfst.RunTests(t, fileSystem, ".file",
			vfst.TestPath("/home/user/.file",
				vfst.TestContentsString("contents of file, count 2\n"),
			),
		)
	})
}
