package tenants

import (
	"bytes"
	"context"
	"os"
	"path/filepath"
	"reflect"
	"testing"
	"time"

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

func TestEntriesFromArchiveDir(t *testing.T) {
	dir := t.TempDir()
	// Layout: dir/<bizType>/<org>/<project>/<cluster>/cluster.yaml
	mustWrite := func(path, body string) {
		full := filepath.Join(dir, path)
		assert.NoError(t, os.MkdirAll(filepath.Dir(full), 0o755))
		assert.NoError(t, os.WriteFile(full, []byte(body), 0o644))
	}
	mustWrite("tidbcloud/o1/p1/c1/cluster.yaml", "bizType: tidbcloud\nvmAccountId: 1001\nvmProjectId: 0\n")
	mustWrite("tidbcloud/o2/p2/c2/cluster.yaml", "bizType: tidbcloud\nvmAccountId: 1002\nvmProjectId: 0\n")
	mustWrite("tidbcloud/o4/p4/c4/not-cluster.yaml", "bizType: tidbcloud\nvmAccountId: 9999\n") // wrong filename
	mustWrite("tidbcloud/o5/p5/c5/cluster.yaml", "bizType: other\nvmAccountId: 1005\n")         // bizType not in list

	bizDir := filepath.Join(dir, "tidbcloud")
	got, err := EntriesFromArchiveDir(bizDir, []string{"tidbcloud", "staging-tidbcloud"}, SourceS3, nil)
	assert.NoError(t, err)
	want := []Entry{
		{Tenant: "1001:0", BizType: "tidbcloud", Source: SourceS3},
		{Tenant: "1002:0", BizType: "tidbcloud", Source: SourceS3},
	}
	assert.True(t, reflect.DeepEqual(got, want), "got %+v want %+v", got, want)
}

func TestEntriesFromArchiveDirWithRelPathFilter(t *testing.T) {
	dir := t.TempDir()
	mustWrite := func(path, body string) {
		full := filepath.Join(dir, path)
		assert.NoError(t, os.MkdirAll(filepath.Dir(full), 0o755))
		assert.NoError(t, os.WriteFile(full, []byte(body), 0o644))
	}
	mustWrite("tidbcloud/o1/p1/c1/cluster.yaml", "bizType: tidbcloud\nvmAccountId: 1001\nvmProjectId: 0\n")
	mustWrite("tidbcloud/o2/p2/c2/cluster.yaml", "bizType: tidbcloud\nvmAccountId: 1002\nvmProjectId: 0\n")

	valid := map[string]bool{"o1/p1/c1/cluster.yaml": true}
	bizDir := filepath.Join(dir, "tidbcloud")
	got, err := EntriesFromArchiveDir(bizDir, []string{"tidbcloud"}, SourceS3, valid)
	assert.NoError(t, err)
	want := []Entry{
		{Tenant: "1001:0", BizType: "tidbcloud", Source: SourceS3},
	}
	assert.True(t, reflect.DeepEqual(got, want), "got %+v want %+v", got, want)
}

func TestEntriesFromArchiveDirMissingRoot(t *testing.T) {
	_, err := EntriesFromArchiveDir(filepath.Join(t.TempDir(), "absent"), []string{"tidbcloud"}, SourceS3, nil)
	assert.Error(t, err)
}

func TestAWSListerList(t *testing.T) {
	dir := t.TempDir()
	argsFile := filepath.Join(dir, "args")
	putOnPATH(t, "aws", recordArgs(argsFile)+`cat <<'EOF'
{"Contents":[
 {"Key":"archived/tidbcloud/o1/p1/c1/cluster.yaml","LastModified":"2026-01-15T12:34:56.000Z"},
 {"Key":"archived/tidbcloud/o1/p1/c1/notes.txt","LastModified":"2026-01-15T12:34:56.000Z"},
 {"Key":"archived/tidbcloud/o2/p2/c2/cluster.yaml","LastModified":""},
 {"Key":"archived/tidbcloud/o3/p3/c3/cluster.yaml","LastModified":"not-a-date"},
 {"Key":"archived/tidbcloud/o4/p4/c4/cluster.yaml","LastModified":"2026-02-15T08:00:00Z"}
]}
EOF`)

	got, err := NewAWSLister().List(context.Background(), "bkt", "tidbcloud")
	assert.NoError(t, err)
	want := []ArchiveObject{
		{Key: "archived/tidbcloud/o1/p1/c1/cluster.yaml", LastModified: time.Date(2026, 1, 15, 12, 34, 56, 0, time.UTC)},
		{Key: "archived/tidbcloud/o4/p4/c4/cluster.yaml", LastModified: time.Date(2026, 2, 15, 8, 0, 0, 0, time.UTC)},
	}
	assert.True(t, reflect.DeepEqual(got, want), "got %+v want %+v", got, want)
	assert.Equal(t, []string{
		"s3api", "list-objects-v2", "--bucket", "bkt",
		"--prefix", "archived/tidbcloud/", "--output", "json",
	}, readArgs(t, argsFile))
}

func TestAWSListerListFailures(t *testing.T) {
	t.Run("CLIFailurePrefersStderr", func(t *testing.T) {
		putOnPATH(t, "aws", `echo "profile not found" 1>&2; exit 2`)
		_, err := NewAWSLister().List(context.Background(), "bkt", "tidbcloud")
		assert.ErrorContains(t, err, "aws s3api list-objects-v2")
		assert.ErrorContains(t, err, "profile not found")
	})

	t.Run("BadJSON", func(t *testing.T) {
		putOnPATH(t, "aws", `echo '{nope'`)
		_, err := NewAWSLister().List(context.Background(), "bkt", "tidbcloud")
		assert.ErrorContains(t, err, "parse aws s3api output")
	})
}

func TestAWSDownloaderDownload(t *testing.T) {
	t.Run("ArgsAndMirroredOutput", func(t *testing.T) {
		dir := t.TempDir()
		argsFile := filepath.Join(dir, "args")
		putOnPATH(t, "aws", recordArgs(argsFile)+
			`echo "download: s3://bkt/archived/tidbcloud/o1/p1/c1/cluster.yaml to dest/o1/cluster.yaml"`+"\n"+
			`echo "warning: retrying" 1>&2`+"\n"+
			`echo "PYTHONUNBUFFERED=$PYTHONUNBUFFERED"`)

		dest := filepath.Join(dir, "dest")
		var out bytes.Buffer
		assert.NoError(t, NewAWSDownloader(&out).Download(context.Background(), "bkt", "tidbcloud", dest))
		assert.Equal(t, []string{
			"s3", "cp", "--recursive", "--exclude", "*",
			"--include", "*/cluster.yaml", "s3://bkt/archived/tidbcloud/", dest + "/",
		}, readArgs(t, argsFile))
		assert.Contains(t, out.String(), "download: s3://bkt/archived/tidbcloud/o1/p1/c1/cluster.yaml")
		assert.Contains(t, out.String(), "warning: retrying")
		assert.Contains(t, out.String(), "PYTHONUNBUFFERED=1")
	})

	t.Run("Failure", func(t *testing.T) {
		putOnPATH(t, "aws", `echo "access denied" 1>&2; exit 1`)
		err := NewAWSDownloader(&bytes.Buffer{}).Download(context.Background(), "bkt", "tidbcloud", t.TempDir())
		assert.ErrorContains(t, err, "aws s3 cp --recursive s3://bkt/archived/tidbcloud/")
		assert.ErrorContains(t, err, "access denied")
	})
}

func TestParseISO8601(t *testing.T) {
	f := func(t *testing.T, in, want string) {
		t.Helper()
		got, err := parseISO8601(in)
		if want == "" {
			assert.Error(t, err, "parseISO8601(%q)", in)
			return
		}
		assert.NoError(t, err, "parseISO8601(%q)", in)
		assert.Equal(t, want, got.Format(time.RFC3339Nano), "parseISO8601(%q)", in)
		assert.Equal(t, time.UTC, got.Location())
	}
	f(t, "2026-01-15T12:34:56.789Z", "2026-01-15T12:34:56.789Z")
	f(t, "2026-01-15T12:34:56Z", "2026-01-15T12:34:56Z")
	f(t, "2026-01-15T12:34:56.000Z", "2026-01-15T12:34:56Z")
	f(t, "2026-01-15T12:34:56+02:00", "2026-01-15T10:34:56Z")
	f(t, "2026-01-15", "")
	f(t, "not-a-date", "")
}
