package server

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

	"github.com/stretchr/testify/assert"

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

func compilePlanBytes(t *testing.T, tenantIDs []string) []byte {
	t.Helper()
	dir := t.TempDir()
	data, err := os.ReadFile(filepath.Join("..", "..", "testdata", "golden", "plan-input.json"))
	assert.NoError(t, err)
	planPath := filepath.Join(dir, "plan.json")
	assert.NoError(t, os.WriteFile(planPath, data, 0o644))
	tenantsJSON, err := json.Marshal(tenantIDs)
	assert.NoError(t, err)
	tenantsPath := filepath.Join(dir, "tenants.json")
	assert.NoError(t, os.WriteFile(tenantsPath, tenantsJSON, 0o644))
	result, err := plan.Compile(plan.CompileOptions{Plan: planPath, Tenants: tenantsPath})
	assert.NoError(t, err)
	compiled, err := os.ReadFile(result.CompilePath)
	assert.NoError(t, err)
	return compiled
}

func TestRegistryPrepare(t *testing.T) {
	migrationsDir := filepath.Join(t.TempDir(), "migrations")
	compiled := compilePlanBytes(t, []string{"1002:0", "1001:0"})

	r := NewRegistry(context.Background(), migrationsDir)
	m, err := r.Prepare("prod-2026-08", compiled)
	assert.NoError(t, err)
	assert.Equal(t, "prod-2026-08", m.Name())

	outDir := filepath.Join(migrationsDir, "prod-2026-08")
	for _, name := range []string{"compiled-plan.json", "migration-loop.json", "tenants-1001-0.json", "tenants-1002-0.json"} {
		_, err := os.Stat(filepath.Join(outDir, name))
		assert.NoError(t, err, "%s: %v", name, err)
	}
	snapshot, err := os.ReadFile(filepath.Join(outDir, "compiled-plan.json"))
	assert.NoError(t, err)
	assert.True(t, string(snapshot) == string(compiled), "compiled-plan snapshot differs from input")

	got, ok := r.Get("prod-2026-08")
	assert.True(t, ok, "Prepare must register the migration")
	assert.Same(t, m, got)
}

func TestRegistryPrepareErrors(t *testing.T) {
	t.Run("MigrationExists", func(t *testing.T) {
		migrationsDir := filepath.Join(t.TempDir(), "migrations")
		compiled := compilePlanBytes(t, []string{"1001:0"})
		r := NewRegistry(context.Background(), migrationsDir)
		_, err := r.Prepare("m1", compiled)
		assert.NoError(t, err)
		_, err = r.Prepare("m1", compiled)
		assert.ErrorIs(t, err, errMigrationExists)
	})

	t.Run("StagingLeftover", func(t *testing.T) {
		migrationsDir := filepath.Join(t.TempDir(), "migrations")
		assert.NoError(t, os.MkdirAll(filepath.Join(migrationsDir, "m1.tmp"), 0o755))
		_, err := prepareToDir(migrationsDir, "m1", []byte(`{}`))
		assert.ErrorIs(t, err, errStagingLeftover)
	})

	t.Run("InvalidPlanGarbage", func(t *testing.T) {
		_, err := prepareToDir(t.TempDir(), "m1", []byte(`{not-json`))
		assert.ErrorIs(t, err, errInvalidPlan)
	})

	t.Run("InvalidPlanNoTenants", func(t *testing.T) {
		data, err := os.ReadFile(filepath.Join("..", "..", "testdata", "golden", "plan-input.json"))
		assert.NoError(t, err)
		_, err = prepareToDir(t.TempDir(), "m1", data)
		assert.ErrorIs(t, err, errInvalidPlan)
	})

	t.Run("InvalidName", func(t *testing.T) {
		_, err := prepareToDir(t.TempDir(), "FOO", []byte(`{}`))
		assert.ErrorContains(t, err, "invalid migration name")
	})
}

func TestCleanupStaging(t *testing.T) {
	staging := filepath.Join(t.TempDir(), "staging-tmp")
	assert.NoError(t, os.MkdirAll(filepath.Join(staging, "nested"), 0o755))
	assert.NoError(t, os.WriteFile(filepath.Join(staging, "nested", "f.json"), []byte("{}"), 0o644))

	cleanupStaging(staging)
	_, err := os.Stat(staging)
	assert.True(t, os.IsNotExist(err), "staging dir must be gone, got err=%v", err)
}

func TestIsValidMigrationName(t *testing.T) {
	f := func(t *testing.T, name string, want bool) {
		t.Helper()
		assert.Equal(t, want, isValidMigrationName(name), "isValidMigrationName(%q)", name)
	}
	f(t, "prod-2026-08", true)
	f(t, "a", true)
	f(t, "0", true)
	f(t, "a-b-c-123", true)
	f(t, "", false)
	f(t, "-leading-dash", false)
	f(t, "trailing-dash-", false)
	f(t, "Upper", false)
	f(t, "under_score", false)
	f(t, "dot.name", false)
	f(t, strings.Repeat("a", 63), true)
	f(t, strings.Repeat("a", 64), false)
}
