package tenants

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"os"
	"path/filepath"
	"testing"
	"time"

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

// assertTenantTotal decodes the entries wire-shape and checks Total structurally.
func assertTenantTotal(t *testing.T, body []byte, want int) {
	t.Helper()
	var got struct {
		Total int `json:"total"`
	}
	if assert.NoError(t, json.Unmarshal(body, &got)) {
		assert.Equal(t, want, got.Total)
	}
}

type fakeKubectlLister struct {
	payload []byte
	err     error
}

func (f *fakeKubectlLister) List(ctx context.Context, selector string) ([]byte, error) {
	if f.err != nil {
		return nil, f.err
	}
	return f.payload, nil
}

type fakeS3Lister struct {
	objects map[string][]ArchiveObject
	err     error
}

func (f *fakeS3Lister) List(ctx context.Context, bucket, bizType string) ([]ArchiveObject, error) {
	if f.err != nil {
		return nil, f.err
	}
	return f.objects[bizType], nil
}

type fakeS3Downloader struct {
	bodies map[string]map[string]string
	err    error
	calls  []struct{ bucket, bizType, destDir string }
}

func (f *fakeS3Downloader) Download(ctx context.Context, bucket, bizType, destDir string) error {
	f.calls = append(f.calls, struct{ bucket, bizType, destDir string }{bucket, bizType, destDir})
	if f.err != nil {
		return f.err
	}
	for rel, body := range f.bodies[bizType] {
		full := filepath.Join(destDir, rel)
		if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
			return err
		}
		if err := os.WriteFile(full, []byte(body), 0o644); err != nil {
			return err
		}
	}
	return nil
}

func TestParseMaxAge(t *testing.T) {
	f := func(in string, wantHrs int, wantErr bool) {
		t.Helper()
		got, err := ParseMaxAge(in)
		if wantErr {
			assert.Error(t, err, "in=%q", in)
			return
		}
		assert.NoError(t, err, "in=%q", in)
		assert.Equal(t, wantHrs, int(got.Hours()), "in=%q", in)
	}
	f("30d", 30*24, false)
	f("90d", 90*24, false)
	f("1d", 24, false)
	f("0d", 0, true)
	f("-1d", 0, true)
	f("30", 0, true)
	f("30h", 0, true)
	f("", 0, true)
	f("abc", 0, true)
}

func TestRunDedicatedK8s(t *testing.T) {
	output := filepath.Join(t.TempDir(), "out.json")
	o := DedicatedK8sOptions{Output: output}
	k8s := &fakeKubectlLister{payload: []byte(`{
  "items": [
    {"metadata": {"labels": {"observability.pingcap.com/bizType": "tidbcloud"}},
     "spec": {"victoriaAccount": {"accountID": "1001", "projectID": "0"}}},
    {"metadata": {"labels": {"observability.pingcap.com/bizType": "tidbcloud"}},
     "spec": {"victoriaAccount": {"accountID": "1002", "projectID": "0"}}}
  ]
}`)}
	var stdout, stderr bytes.Buffer
	assert.NoError(t, RunDedicatedK8s(context.Background(), o, k8s, &stdout, &stderr))

	body := mustReadFile(t, output)
	assertTenantTotal(t, body, 2)
	for _, want := range []string{`"1001:0"`, `"1002:0"`, `"bizType": "tidbcloud"`, `"source": "k8s"`} {
		assert.Contains(t, string(body), want)
	}
	for _, want := range []string{"discovering from kubernetes...", "kubernetes: 2 tenant(s)", "wrote 2 tenants to"} {
		assert.Contains(t, stderr.String(), want)
	}
}

func TestRunDedicatedK8sNoTenants(t *testing.T) {
	k8s := &fakeKubectlLister{payload: []byte(`{"items": []}`)}
	err := RunDedicatedK8s(context.Background(), DedicatedK8sOptions{}, k8s, &bytes.Buffer{}, &bytes.Buffer{})
	assert.ErrorContains(t, err, "no tenants found")
}

func TestRunDedicatedK8sPropagatesError(t *testing.T) {
	k8s := &fakeKubectlLister{err: errors.New("kubectl exploded")}
	err := RunDedicatedK8s(context.Background(), DedicatedK8sOptions{}, k8s, &bytes.Buffer{}, &bytes.Buffer{})
	assert.ErrorContains(t, err, "kubectl exploded")
}

func TestRunDedicatedS3(t *testing.T) {
	output := filepath.Join(t.TempDir(), "out.json")
	o := DedicatedS3Options{S3Bucket: "bkt", MaxAge: "90d", Output: output}
	now := time.Now().UTC()
	lister := &fakeS3Lister{objects: map[string][]ArchiveObject{
		"tidbcloud": {
			{Key: "archived/tidbcloud/o1/p1/c1/cluster.yaml", LastModified: now.Add(-1 * 24 * time.Hour)},
			{Key: "archived/tidbcloud/o2/p2/c2/cluster.yaml", LastModified: now.Add(-1 * 24 * time.Hour)},
		},
		"staging-tidbcloud": {},
		"dev-tidbcloud":     {},
	}}
	downloader := &fakeS3Downloader{bodies: map[string]map[string]string{
		"tidbcloud": {
			"o1/p1/c1/cluster.yaml": "bizType: tidbcloud\nvmAccountId: 1001\nvmProjectId: 0\n",
			"o2/p2/c2/cluster.yaml": "bizType: tidbcloud\nvmAccountId: 1002\nvmProjectId: 0\n",
		},
	}}
	var stdout, stderr bytes.Buffer
	assert.NoError(t, RunDedicatedS3(context.Background(), o, lister, downloader, &stdout, &stderr))

	body := mustReadFile(t, output)
	assertTenantTotal(t, body, 2)
	for _, want := range []string{`"1001:0"`, `"1002:0"`, `"source": "s3"`} {
		assert.Contains(t, string(body), want)
	}
	for _, want := range []string{
		"max-age=90d",
		"tidbcloud: 2/2 cluster.yaml within 90d",
		"tidbcloud: downloading archive (2 files)...",
		"tidbcloud: downloaded 2 cluster.yaml in ",
	} {
		assert.Contains(t, stderr.String(), want)
	}
}

func TestRunDedicatedS3MaxAgeFiltersArchive(t *testing.T) {
	output := filepath.Join(t.TempDir(), "out.json")
	o := DedicatedS3Options{S3Bucket: "bkt", MaxAge: "30d", Output: output}
	now := time.Now().UTC()
	lister := &fakeS3Lister{objects: map[string][]ArchiveObject{
		"tidbcloud": {
			{Key: "archived/tidbcloud/o1/p1/c1/cluster.yaml", LastModified: now.Add(-1 * 24 * time.Hour)},
			{Key: "archived/tidbcloud/o2/p2/c2/cluster.yaml", LastModified: now.Add(-100 * 24 * time.Hour)},
		},
		"staging-tidbcloud": {},
		"dev-tidbcloud":     {},
	}}
	downloader := &fakeS3Downloader{bodies: map[string]map[string]string{
		"tidbcloud": {
			"o1/p1/c1/cluster.yaml": "bizType: tidbcloud\nvmAccountId: 1001\nvmProjectId: 0\n",
			"o2/p2/c2/cluster.yaml": "bizType: tidbcloud\nvmAccountId: 1002\nvmProjectId: 0\n",
		},
	}}
	var stdout, stderr bytes.Buffer
	assert.NoError(t, RunDedicatedS3(context.Background(), o, lister, downloader, &stdout, &stderr))

	body := string(mustReadFile(t, output))
	assertTenantTotal(t, []byte(body), 1)
	assert.Contains(t, body, `"1001:0"`)
	assert.NotContains(t, body, `"1002:0"`)
	assert.Contains(t, stderr.String(), "tidbcloud: 1/2 cluster.yaml within 30d")
}

func TestRunDedicatedS3NoTenants(t *testing.T) {
	o := DedicatedS3Options{S3Bucket: "bkt", MaxAge: "90d"}
	lister := &fakeS3Lister{objects: map[string][]ArchiveObject{
		"tidbcloud":         {},
		"staging-tidbcloud": {},
		"dev-tidbcloud":     {},
	}}
	err := RunDedicatedS3(context.Background(), o, lister, &fakeS3Downloader{}, &bytes.Buffer{}, &bytes.Buffer{})
	assert.ErrorContains(t, err, "no tenants found")
}
