package status

import (
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"

	"github.com/tidbcloud/observability/vm-migration/internal/jsonio"
	"github.com/tidbcloud/observability/vm-migration/internal/schema"
)

func TestStatusPathForManifest(t *testing.T) {
	cases := []struct {
		name string
		in   string
		want string
	}{
		{"plain stem", "tenants-1-manifest.json", "tenants-1-status.json"},
		{"nested path", "envs/prod/tenants-1-manifest.json", filepath.ToSlash("envs/prod/tenants-1-status.json")},
		{"no manifest suffix", "weird-name.json", "weird-name-status.json"},
		{"with directory and no manifest suffix", "envs/prod/foo.json", filepath.ToSlash("envs/prod/foo-status.json")},
		{"manifest suffix only", "x-manifest.json", "x-status.json"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			assert.Equal(t, tc.want, PathForManifest(tc.in))
		})
	}
}

func TestBuildSummaryCounts(t *testing.T) {
	doc := &schema.Document{
		Shards: []schema.Shard{
			{Phase: schema.PhaseImported, Bytes: int64Ptr(1024)},
			{Phase: schema.PhaseImported, Bytes: int64Ptr(2048)},
			{Phase: schema.PhasePending},
			{Phase: schema.PhaseFailed},
			{Phase: schema.PhaseImported, Verified: true},
		},
	}
	s := BuildSummary(doc)
	assert.Equal(t, 5, s.Total)
	assert.Equal(t, 3, s.Imported)
	assert.Equal(t, 1, s.Verified)
	assert.Equal(t, 1, s.Failed)
	assert.Equal(t, 1, s.Pending)
	assert.Equal(t, int64(3072), s.TotalBytes)
}

func TestBuildSummaryEmptyPhase(t *testing.T) {
	doc := &schema.Document{
		Shards: []schema.Shard{
			{Phase: schema.PhasePending},
			{Phase: schema.PhaseUnknown},
		},
	}
	s := BuildSummary(doc)
	// pending = total - imported - failed; unknown-phase shards count as pending.
	assert.Equal(t, 2, s.Pending)
	assert.Equal(t, 2, s.Total)
}

func TestWriteAndLoadJSONAtomicRoundTrip(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "out.json")

	original := &schema.Document{
		ManifestRef: "test-manifest.json",
		Region:      "prod",
		Shards: []schema.Shard{
			{ShardID: "1001:0:1", Tenant: "1001:0", Phase: schema.PhaseImported, Bytes: int64Ptr(4096)},
			{ShardID: "1001:0:2", Tenant: "1001:0", Phase: schema.PhasePending},
		},
	}
	assert.NoError(t, WriteJSONAtomic(path, original))

	loaded, err := LoadDocument(path)
	assert.NoError(t, err)
	assert.Equal(t, original.ManifestRef, loaded.ManifestRef)
	assert.Len(t, loaded.Shards, 2)
	assert.Equal(t, schema.PhaseImported, loaded.Shards[0].Phase)
	assert.NotNil(t, loaded.Shards[0].Bytes)
	assert.Equal(t, int64(4096), *loaded.Shards[0].Bytes)
	assert.Equal(t, 2, loaded.Summary.Total)
	assert.Equal(t, 1, loaded.Summary.Imported)
}

func TestWriteJSONAtomicIsAtomic(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "out.json")
	assert.NoError(t, os.WriteFile(path, []byte("original\n"), 0o644))
	doc := &schema.Document{Shards: []schema.Shard{{Phase: schema.PhasePending}}}
	assert.NoError(t, WriteJSONAtomic(path, doc))
	data, err := os.ReadFile(path)
	assert.NoError(t, err)
	assert.NotContains(t, string(data), "original", "atomic rename failed: file still contains old content")
	entries, _ := os.ReadDir(dir)
	for _, e := range entries {
		assert.False(t, strings.HasSuffix(e.Name(), ".tmp") || strings.Contains(e.Name(), ".tmp."),
			"left-behind tmp file: %s", e.Name())
	}
}

func TestWriteJSONAtomicDoesNotEscapeHTML(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "out.json")
	lastErr := `filter {__name__=~"foo>bar"} failed <fast>`
	doc := &schema.Document{
		Shards: []schema.Shard{
			{ShardID: "x", Tenant: "y", Phase: schema.PhaseFailed, LastError: &lastErr},
		},
	}
	assert.NoError(t, WriteJSONAtomic(path, doc))
	data, err := os.ReadFile(path)
	assert.NoError(t, err)
	s := string(data)
	for _, want := range []string{`<fast>`, `foo>bar`, `{__name__=~`} {
		assert.Contains(t, s, want, "expected literal %q in status JSON; got escaped output", want)
	}
	badSequences := []string{"\\u003c", "\\u003e", "\\u0026"}
	for _, bad := range badSequences {
		assert.NotContains(t, s, bad, "HTML-escape artifact %q present in status JSON", bad)
	}
}

func TestDumpJSONAtomicNonStatus(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "x.json")
	v := map[string]any{"kind": "test", "items": []any{1, 2, 3}}
	assert.NoError(t, DumpJSONAtomic(path, v))
	data, err := os.ReadFile(path)
	assert.NoError(t, err)
	var got map[string]any
	assert.NoError(t, json.Unmarshal(data, &got))
	assert.Equal(t, "test", got["kind"])
}

func TestBuildInitialStatus(t *testing.T) {
	m := &schema.Manifest{
		Region:    "prod",
		Namespace: "observability",
		Vmcluster: "vmcluster",
		Tenants: []schema.ManifestTenant{
			{
				Tenant: "1001:0",
				Shards: []schema.ManifestShard{
					{ShardID: "1001:0:1", TimeStart: "2026-06-16T00:00:00Z", TimeEnd: "2026-06-17T00:00:00Z"},
					{ShardID: "1001:0:2", TimeStart: "2026-06-17T00:00:00Z", TimeEnd: "2026-06-18T00:00:00Z"},
				},
			},
		},
	}
	doc := BuildInitialStatus(m, "tenants-1-manifest.json")
	assert.Equal(t, "tenants-1-manifest.json", doc.ManifestRef)
	assert.Len(t, doc.Shards, 2)
	assert.Equal(t, schema.PhasePending, doc.Shards[0].Phase)
	assert.Equal(t, "1001:0:1", doc.Shards[0].ShardID)
}

func TestIterShards(t *testing.T) {
	m := &schema.Manifest{
		Tenants: []schema.ManifestTenant{
			{Tenant: "1001:0", Shards: []schema.ManifestShard{{ShardID: "1001:0:1"}}},
			{Tenant: "1002:0", Shards: []schema.ManifestShard{{ShardID: "1002:0:1"}, {ShardID: "1002:0:2"}}},
		},
	}
	shards := IterShards(m)
	assert.Len(t, shards, 3)
	assert.Equal(t, "1001:0", shards[0].Tenant)
	assert.Equal(t, "1002:0", shards[2].Tenant)
}

func TestFindShard(t *testing.T) {
	doc := &schema.Document{Shards: []schema.Shard{
		{ShardID: "1001:0:1", Phase: schema.PhasePending},
		{ShardID: "1001:0:2", Phase: schema.PhasePending},
	}}

	s := FindShard(doc, "1001:0:2")
	if assert.NotNil(t, s) {
		s.Phase = schema.PhaseImported
		assert.Equal(t, schema.PhaseImported, doc.Shards[1].Phase,
			"FindShard must alias doc.Shards so callers can mutate through it")
	}

	assert.Nil(t, FindShard(doc, "absent"))
	assert.Nil(t, FindShard(&schema.Document{}, "1001:0:1"))
}

func TestLoadManifest(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "m-manifest.json")
	in := &schema.Manifest{
		Region: "prod",
		Tenants: []schema.ManifestTenant{
			{Tenant: "1001:0", Shards: []schema.ManifestShard{{ShardID: "1001:0:1"}}},
		},
	}
	assert.NoError(t, jsonio.WriteFile(path, in))

	got, err := LoadManifest(path)
	assert.NoError(t, err)
	assert.Equal(t, in.Region, got.Region)
	assert.Equal(t, "1001:0:1", got.Tenants[0].Shards[0].ShardID)

	_, err = LoadManifest(filepath.Join(dir, "absent.json"))
	assert.Error(t, err)

	bad := filepath.Join(dir, "bad.json")
	assert.NoError(t, os.WriteFile(bad, []byte("{oops"), 0o600))
	_, err = LoadManifest(bad)
	assert.Error(t, err)
}

func TestWriteJSONAtomicFailures(t *testing.T) {
	dir := t.TempDir()

	t.Run("MarshalError", func(t *testing.T) {
		err := DumpJSONAtomic(filepath.Join(dir, "x.json"), map[string]any{"ch": make(chan int)})
		assert.Error(t, err)
	})

	t.Run("DirCreationFailure", func(t *testing.T) {
		blocker := filepath.Join(dir, "blocker")
		assert.NoError(t, os.WriteFile(blocker, []byte("{}"), 0o600))
		err := DumpJSONAtomic(filepath.Join(blocker, "sub", "x.json"), map[string]any{})
		assert.Error(t, err)
	})
}

func int64Ptr(n int64) *int64 { return new(n) }
