package restic

import (
	"errors"
	"os"
	"path/filepath"
	"testing"
	"time"

	"git.secluded.site/keld/internal/config"
)

// fixtureLsJSON is real output from:
//
//	keld --preset music-final@b2 ls --json --recursive 8bd729b1 /home/amolith/Music/Final/3nd
//
// 24 lines: 1 snapshot header + 4 directories + 19 .flac files across 3 album
// directories. The snapshot header line has message_type "snapshot" and must be
// skipped; all remaining lines have message_type "node".
const fixtureLsJSON = `{"time":"2026-03-24T00:20:53.867687549-06:00","parent":"2a1eb755327d446b938ef5a6e991f83f86abf12911f3bc87602fc696a7d1d4af","tree":"011fe38da2e96cb6ea9b2bb8bbc710d5b92ab9f4ea2d91be089bba532d329aae","paths":["/home/amolith/Music/Final"],"hostname":"angmar","username":"amolith","uid":1000,"gid":1000,"program_version":"restic 0.18.1","summary":{"backup_start":"2026-03-24T00:20:53.867687549-06:00","backup_end":"2026-03-24T00:20:58.420948969-06:00","files_new":0,"files_changed":4,"files_unmodified":6010,"dirs_new":0,"dirs_changed":4,"dirs_unmodified":862,"data_blobs":1,"tree_blobs":5,"data_added":89361,"data_added_packed":13072,"total_files_processed":6014,"total_bytes_processed":121289853644},"id":"8bd729b16043cef621423ca2f6100832e8add471374a3a54c555d624b011988c","short_id":"8bd729b1","message_type":"snapshot","struct_type":"snapshot"}
{"name":"3nd","type":"dir","path":"/home/amolith/Music/Final/3nd","uid":1000,"gid":1000,"mode":2147484157,"permissions":"drwxrwxr-x","mtime":"2021-04-17T18:01:57.376612-06:00","atime":"2021-04-17T18:01:57.376612-06:00","ctime":"2024-09-26T19:41:45.573886986-06:00","inode":42207275,"message_type":"node","struct_type":"node"}
{"name":"World Tour","type":"dir","path":"/home/amolith/Music/Final/3nd/World Tour","uid":1000,"gid":1000,"mode":2147484157,"permissions":"drwxrwxr-x","mtime":"2024-06-10T09:00:35.86776-06:00","atime":"2024-06-10T09:00:35.86776-06:00","ctime":"2024-09-26T19:41:45.563886805-06:00","inode":42207421,"message_type":"node","struct_type":"node"}
{"name":"01 - monsoon.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/01 - monsoon.flac","uid":1000,"gid":1000,"size":30037820,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:20.840315-07:00","atime":"2021-11-25T17:31:20.840315-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42213020,"message_type":"node","struct_type":"node"}
{"name":"02 - china.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/02 - china.flac","uid":1000,"gid":1000,"size":17705986,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:22.00033-07:00","atime":"2021-11-25T17:31:22.00033-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42213321,"message_type":"node","struct_type":"node"}
{"name":"03 - SSK.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/03 - SSK.flac","uid":1000,"gid":1000,"size":32263802,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:22.753673-07:00","atime":"2021-11-25T17:31:22.753673-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42213685,"message_type":"node","struct_type":"node"}
{"name":"04 - midroll.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/04 - midroll.flac","uid":1000,"gid":1000,"size":37431281,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:23.643684-07:00","atime":"2021-11-25T17:31:23.643684-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42213686,"message_type":"node","struct_type":"node"}
{"name":"05 - LOTUS.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/05 - LOTUS.flac","uid":1000,"gid":1000,"size":33509064,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:24.157024-07:00","atime":"2021-11-25T17:31:24.157024-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42213687,"message_type":"node","struct_type":"node"}
{"name":"06 - 眠る.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/06 - 眠る.flac","uid":1000,"gid":1000,"size":21319060,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:24.383693-07:00","atime":"2021-11-25T17:31:24.383693-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42213249,"message_type":"node","struct_type":"node"}
{"name":"07 - filter in dust.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/07 - filter in dust.flac","uid":1000,"gid":1000,"size":21849635,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:24.613696-07:00","atime":"2021-11-25T17:31:24.613696-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42213250,"message_type":"node","struct_type":"node"}
{"name":"08 - 夏終る.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/08 - 夏終る.flac","uid":1000,"gid":1000,"size":28061640,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:24.817032-07:00","atime":"2021-11-25T17:31:24.817032-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42212532,"message_type":"node","struct_type":"node"}
{"name":"view from here","type":"dir","path":"/home/amolith/Music/Final/3nd/view from here","uid":1000,"gid":1000,"mode":2147484157,"permissions":"drwxrwxr-x","mtime":"2024-06-10T09:00:35.874427-06:00","atime":"2024-06-10T09:00:35.874427-06:00","ctime":"2024-09-26T19:41:45.570553593-06:00","inode":42207422,"message_type":"node","struct_type":"node"}
{"name":"01 - clockworker.flac","type":"file","path":"/home/amolith/Music/Final/3nd/view from here/01 - clockworker.flac","uid":1000,"gid":1000,"size":38342556,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:12.526878-07:00","atime":"2021-11-25T17:31:12.526878-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42214320,"message_type":"node","struct_type":"node"}
{"name":"02 - augustline.flac","type":"file","path":"/home/amolith/Music/Final/3nd/view from here/02 - augustline.flac","uid":1000,"gid":1000,"size":27357365,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:13.413555-07:00","atime":"2021-11-25T17:31:13.413555-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42212371,"message_type":"node","struct_type":"node"}
{"name":"03 - season.flac","type":"file","path":"/home/amolith/Music/Final/3nd/view from here/03 - season.flac","uid":1000,"gid":1000,"size":26918886,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:14.256899-07:00","atime":"2021-11-25T17:31:14.256899-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42212245,"message_type":"node","struct_type":"node"}
{"name":"04 - ハルオト.flac","type":"file","path":"/home/amolith/Music/Final/3nd/view from here/04 - ハルオト.flac","uid":1000,"gid":1000,"size":45722382,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:15.253578-07:00","atime":"2021-11-25T17:31:15.253578-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42212533,"message_type":"node","struct_type":"node"}
{"name":"05 - walk in the brown.flac","type":"file","path":"/home/amolith/Music/Final/3nd/view from here/05 - walk in the brown.flac","uid":1000,"gid":1000,"size":38267170,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:16.300258-07:00","atime":"2021-11-25T17:31:16.300258-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42214321,"message_type":"node","struct_type":"node"}
{"name":"we dance × avec toi","type":"dir","path":"/home/amolith/Music/Final/3nd/we dance × avec toi","uid":1000,"gid":1000,"mode":2147484157,"permissions":"drwxrwxr-x","mtime":"2024-06-10T09:00:35.87776-06:00","atime":"2024-06-10T09:00:35.87776-06:00","ctime":"2024-09-26T19:41:45.573886986-06:00","inode":42207423,"message_type":"node","struct_type":"node"}
{"name":"01 - Bender.flac","type":"file","path":"/home/amolith/Music/Final/3nd/we dance × avec toi/01 - Bender.flac","uid":1000,"gid":1000,"size":24044464,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:17.563608-07:00","atime":"2021-11-25T17:31:17.563608-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42211884,"message_type":"node","struct_type":"node"}
{"name":"02 - テレプシコラー.flac","type":"file","path":"/home/amolith/Music/Final/3nd/we dance × avec toi/02 - テレプシコラー.flac","uid":1000,"gid":1000,"size":29003735,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:18.106948-07:00","atime":"2021-11-25T17:31:18.106948-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42211885,"message_type":"node","struct_type":"node"}
{"name":"03 - コロコロ転がる.flac","type":"file","path":"/home/amolith/Music/Final/3nd/we dance × avec toi/03 - コロコロ転がる.flac","uid":1000,"gid":1000,"size":27127530,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:18.643621-07:00","atime":"2021-11-25T17:31:18.643621-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42212285,"message_type":"node","struct_type":"node"}
{"name":"04 - algorhythm.flac","type":"file","path":"/home/amolith/Music/Final/3nd/we dance × avec toi/04 - algorhythm.flac","uid":1000,"gid":1000,"size":36728278,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:19.070293-07:00","atime":"2021-11-25T17:31:19.070293-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42212286,"message_type":"node","struct_type":"node"}
{"name":"05 - Rain Song.flac","type":"file","path":"/home/amolith/Music/Final/3nd/we dance × avec toi/05 - Rain Song.flac","uid":1000,"gid":1000,"size":27059422,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:19.540299-07:00","atime":"2021-11-25T17:31:19.540299-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42212287,"message_type":"node","struct_type":"node"}
{"name":"06 - untroubled terror.flac","type":"file","path":"/home/amolith/Music/Final/3nd/we dance × avec toi/06 - untroubled terror.flac","uid":1000,"gid":1000,"size":6965876,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:19.853636-07:00","atime":"2021-11-25T17:31:19.853636-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42211886,"message_type":"node","struct_type":"node"}
`

func TestParseLsNodes(t *testing.T) {
	t.Parallel()

	nodes, err := ParseLsNodes([]byte(fixtureLsJSON))
	if err != nil {
		t.Fatalf("ParseLsNodes(): unexpected error: %v", err)
	}

	// 23 node lines (4 dirs + 19 files), snapshot header skipped.
	if got := len(nodes); got != 23 {
		t.Fatalf("len(nodes): got %d, want 23", got)
	}

	// First node should be the root dir of the listing.
	first := nodes[0]
	if first.Name != "3nd" {
		t.Errorf("first node name: got %q, want %q", first.Name, "3nd")
	}
	if first.Type != "dir" {
		t.Errorf("first node type: got %q, want %q", first.Type, "dir")
	}
	if first.Path != "/home/amolith/Music/Final/3nd" {
		t.Errorf("first node path: got %q, want %q", first.Path, "/home/amolith/Music/Final/3nd")
	}
	if first.Size != 0 {
		t.Errorf("first node size: got %d, want 0 (dirs have no size)", first.Size)
	}
	if first.Mode != 2147484157 {
		t.Errorf("first node mode: got %d, want %d", first.Mode, 2147484157)
	}
	if first.Permissions != "drwxrwxr-x" {
		t.Errorf("first node permissions: got %q, want %q", first.Permissions, "drwxrwxr-x")
	}
	wantMtime, _ := time.Parse(time.RFC3339Nano, "2021-04-17T18:01:57.376612-06:00")
	if !first.Mtime.Equal(wantMtime) {
		t.Errorf("first node mtime: got %v, want %v", first.Mtime, wantMtime)
	}

	// Spot-check a file node: first .flac in "World Tour".
	monsoon := nodes[2]
	if monsoon.Name != "01 - monsoon.flac" {
		t.Errorf("monsoon name: got %q, want %q", monsoon.Name, "01 - monsoon.flac")
	}
	if monsoon.Type != "file" {
		t.Errorf("monsoon type: got %q, want %q", monsoon.Type, "file")
	}
	if monsoon.Path != "/home/amolith/Music/Final/3nd/World Tour/01 - monsoon.flac" {
		t.Errorf("monsoon path: got %q, want %q",
			monsoon.Path, "/home/amolith/Music/Final/3nd/World Tour/01 - monsoon.flac")
	}
	if monsoon.Size != 30037820 {
		t.Errorf("monsoon size: got %d, want %d", monsoon.Size, 30037820)
	}
	if monsoon.Mode != 509 {
		t.Errorf("monsoon mode: got %d, want %d", monsoon.Mode, 509)
	}
	if monsoon.Permissions != "-rwxrwxr-x" {
		t.Errorf("monsoon permissions: got %q, want %q", monsoon.Permissions, "-rwxrwxr-x")
	}

	// Spot-check a file with Unicode in the name.
	nemuru := nodes[7]
	if nemuru.Name != "06 - 眠る.flac" {
		t.Errorf("nemuru name: got %q, want %q", nemuru.Name, "06 - 眠る.flac")
	}
	if nemuru.Size != 21319060 {
		t.Errorf("nemuru size: got %d, want %d", nemuru.Size, 21319060)
	}

	// Count dirs vs files.
	var dirs, files int
	for _, n := range nodes {
		switch n.Type {
		case "dir":
			dirs++
		case "file":
			files++
		default:
			t.Errorf("unexpected node type %q for %q", n.Type, n.Path)
		}
	}
	if dirs != 4 {
		t.Errorf("dir count: got %d, want 4", dirs)
	}
	if files != 19 {
		t.Errorf("file count: got %d, want 19", files)
	}
}

func TestParseLsNodesEmpty(t *testing.T) {
	t.Parallel()

	nodes, err := ParseLsNodes([]byte(""))
	if err != nil {
		t.Fatalf("ParseLsNodes(empty): unexpected error: %v", err)
	}
	if len(nodes) != 0 {
		t.Errorf("expected 0 nodes from empty input, got %d", len(nodes))
	}
}

func TestParseLsNodesSnapshotOnly(t *testing.T) {
	t.Parallel()

	// A snapshot header with no node lines — e.g. an empty snapshot or a
	// path filter that matched nothing.
	input := `{"time":"2026-03-24T00:20:53.867687549-06:00","paths":["/home/amolith/Music/Final"],"hostname":"angmar","id":"8bd729b1","short_id":"8bd729b1","message_type":"snapshot","struct_type":"snapshot"}
`

	nodes, err := ParseLsNodes([]byte(input))
	if err != nil {
		t.Fatalf("ParseLsNodes(snapshot-only): unexpected error: %v", err)
	}
	if len(nodes) != 0 {
		t.Errorf("expected 0 nodes from snapshot-only input, got %d", len(nodes))
	}
}

func TestBuildLsCmd(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name     string
		cfg      *config.ResolvedConfig
		snapshot string
		wantArgv []string
		wantErr  bool
	}{
		{
			name: "basic repo flag",
			cfg: &config.ResolvedConfig{
				Command: "restore",
				Flags: []config.Flag{
					{Name: "--repo", Value: "/srv/backup"},
					{Name: "--target", Value: "/tmp/restore"},
				},
			},
			snapshot: "8bd729b1",
			wantArgv: []string{"restic", "ls", "--json", "--repo", "/srv/backup", "8bd729b1"},
		},
		{
			name: "repo with password-file and non-global flags stripped",
			cfg: &config.ResolvedConfig{
				Command: "restore",
				Flags: []config.Flag{
					{Name: "--repo", Value: "rclone:remote:/backup"},
					{Name: "--password-file", Value: "/etc/restic/pw"},
					{Name: "--target", Value: "/tmp/restore"},
					{Name: "--overwrite", Value: "if-changed"},
				},
			},
			snapshot: "latest",
			wantArgv: []string{
				"restic", "ls", "--json",
				"--repo", "rclone:remote:/backup",
				"--password-file", "/etc/restic/pw",
				"latest",
			},
		},
		{
			name: "cache-dir and no-lock forwarded",
			cfg: &config.ResolvedConfig{
				Command: "restore",
				Flags: []config.Flag{
					{Name: "--repo", Value: "/srv/backup"},
					{Name: "--cache-dir", Value: "/tmp/cache"},
					{Name: "--no-lock"},
					{Name: "--target", Value: "/tmp/restore"},
				},
			},
			snapshot: "abc123",
			wantArgv: []string{
				"restic", "ls", "--json",
				"--repo", "/srv/backup",
				"--cache-dir", "/tmp/cache",
				"--no-lock",
				"abc123",
			},
		},
		{
			name: "snapshot selectors forwarded for latest",
			cfg: &config.ResolvedConfig{
				Command: "restore",
				Flags: []config.Flag{
					{Name: "--repo", Value: "/srv/backup"},
					{Name: "--host", Value: "angmar"},
					{Name: "--path", Value: "/home/amolith/Music"},
					{Name: "--tag", Value: "daily"},
					{Name: "--target", Value: "/tmp/restore"},
				},
			},
			snapshot: "latest",
			wantArgv: []string{
				"restic", "ls", "--json",
				"--repo", "/srv/backup",
				"--host", "angmar",
				"--path", "/home/amolith/Music",
				"--tag", "daily",
				"latest",
			},
		},
		{
			name: "short host flag forwarded",
			cfg: &config.ResolvedConfig{
				Command: "restore",
				Flags: []config.Flag{
					{Name: "--repo", Value: "/srv/backup"},
					{Name: "-H", Value: "angmar"},
				},
			},
			snapshot: "latest",
			wantArgv: []string{
				"restic", "ls", "--json",
				"--repo", "/srv/backup",
				"-H", "angmar",
				"latest",
			},
		},
		{
			name: "repo via environ only",
			cfg: &config.ResolvedConfig{
				Command: "restore",
				Flags: []config.Flag{
					{Name: "--target", Value: "/tmp/restore"},
				},
				Environ: map[string]string{
					"RESTIC_REPOSITORY": "/srv/backup",
				},
			},
			snapshot: "8bd729b1",
			wantArgv: []string{"restic", "ls", "--json", "8bd729b1"},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			argv, err := buildLsCmd(tt.cfg, tt.snapshot)
			if (err != nil) != tt.wantErr {
				t.Fatalf("buildLsCmd(): err=%v, wantErr=%v", err, tt.wantErr)
			}
			if err != nil {
				return
			}

			if len(argv) != len(tt.wantArgv) {
				t.Fatalf("argv length: got %d %v, want %d %v",
					len(argv), argv, len(tt.wantArgv), tt.wantArgv)
			}
			for i := range argv {
				if argv[i] != tt.wantArgv[i] {
					t.Errorf("argv[%d]: got %q, want %q", i, argv[i], tt.wantArgv[i])
				}
			}
		})
	}
}

func TestBuildLsCmdNoRepo(t *testing.T) {
	t.Setenv("RESTIC_REPOSITORY", "")
	t.Setenv("RESTIC_REPOSITORY_FILE", "")

	cfg := &config.ResolvedConfig{
		Command: "restore",
		Flags: []config.Flag{
			{Name: "--target", Value: "/tmp/restore"},
		},
	}

	_, err := buildLsCmd(cfg, "8bd729b1")
	if !errors.Is(err, ErrNoRepo) {
		t.Errorf("expected ErrNoRepo, got %v", err)
	}
}

// testdataRepoPath returns the absolute path to the committed test
// restic repository. Tests using this fixture require restic to be
// installed.
func testdataRepoPath(t *testing.T) string {
	t.Helper()
	// Discover the repo relative to this test file's package dir.
	path, err := filepath.Abs("testdata/repo")
	if err != nil {
		t.Fatalf("resolving testdata/repo: %v", err)
	}
	if _, err := os.Stat(path); err != nil {
		t.Fatalf("testdata/repo not found at %s: %v", path, err)
	}
	return path
}

func TestRunLs(t *testing.T) {
	t.Parallel()

	repoPath := testdataRepoPath(t)
	cfg := &config.ResolvedConfig{
		Command: "restore",
		Flags: []config.Flag{
			{Name: "--repo", Value: repoPath},
		},
		Environ: map[string]string{
			"RESTIC_PASSWORD": "test",
		},
	}

	nodes, err := RunLs(cfg, "03b061a6")
	if err != nil {
		t.Fatalf("RunLs(): unexpected error: %v", err)
	}

	// The test repo has 29 files + 26 dirs = 55 nodes.
	if got := len(nodes); got != 55 {
		t.Fatalf("len(nodes): got %d, want 55", got)
	}

	// Count types.
	var dirs, files int
	for _, n := range nodes {
		switch n.Type {
		case "dir":
			dirs++
		case "file":
			files++
		default:
			t.Errorf("unexpected node type %q for %q", n.Type, n.Path)
		}
	}
	if dirs != 26 {
		t.Errorf("dir count: got %d, want 26", dirs)
	}
	if files != 29 {
		t.Errorf("file count: got %d, want 29", files)
	}

	// Spot-check a Unicode path.
	var found bool
	for _, n := range nodes {
		if n.Name == "01 - 夜明け.flac" {
			found = true
			if n.Type != "file" {
				t.Errorf("夜明け type: got %q, want %q", n.Type, "file")
			}
			break
		}
	}
	if !found {
		t.Error("expected to find node '01 - 夜明け.flac'")
	}

	// Spot-check a space-in-path entry.
	found = false
	for _, n := range nodes {
		if n.Name == "Jazz Café" {
			found = true
			if n.Type != "dir" {
				t.Errorf("Jazz Café type: got %q, want %q", n.Type, "dir")
			}
			break
		}
	}
	if !found {
		t.Error("expected to find node 'Jazz Café'")
	}
}
