package migration

import (
	"bytes"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"sync/atomic"
	"testing"

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

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

func actionTestPlan() *schema.MigrationLoopPlan {
	return &schema.MigrationLoopPlan{
		Runtime: &schema.MigrationLoopRuntime{
			Region:    "prod",
			Vmcluster: "vmcluster",
			Vminsert:  "http://insert/{tenant}",
			Window: schema.MigrationLoopWindow{
				Start:        "2026-06-16T00:00:00Z",
				End:          "2026-06-18T00:00:00Z",
				StepInterval: "1d",
			},
			Verification: schema.MigrationLoopVerification{TimeoutSeconds: 5, Tolerance: 0.01},
		},
		Vmselect: &schema.MigrationLoopVmselect{
			Origin:    "http://origin/{tenant}",
			Migration: "http://migration/{tenant}",
			Candidate: "http://candidate/{tenant}",
		},
	}
}

func TestStructuredOptions(t *testing.T) {
	dir := t.TempDir()
	plan := &schema.MigrationLoopPlan{
		Runtime: &schema.MigrationLoopRuntime{
			Region:       "prod",
			Vmcluster:    "vmcluster",
			Vminsert:     "http://insert/{tenant}",
			Window:       schema.MigrationLoopWindow{Start: "start", End: "end", StepInterval: "6h"},
			Verification: schema.MigrationLoopVerification{TimeoutSeconds: 45, Tolerance: 0.02},
		},
		Vmselect: &schema.MigrationLoopVmselect{Origin: "http://origin", Migration: "http://migration", Candidate: "http://candidate"},
	}

	t.Run("Manifest", func(t *testing.T) {
		action := &schema.MigrationLoopAction{Kind: schema.ActionManifest, TenantsFile: "tenants.json", Manifest: "manifest.json"}
		got, err := manifestOptions(plan, dir, action)
		require.NoError(t, err)
		assert.Equal(t, filepath.Join(dir, "tenants.json"), got.TenantsFile)
		assert.Equal(t, filepath.Join(dir, "manifest.json"), got.Output)
		assert.Equal(t, "prod", got.Region)
		assert.Equal(t, "vmcluster", got.Vmcluster)
		assert.Equal(t, "start", got.Start)
		assert.Equal(t, "end", got.End)
		assert.Equal(t, "6h", got.StepInterval)
		assert.Equal(t, "http://migration", got.SrcVmselectURL)
		assert.Equal(t, "http://insert/{tenant}", got.DstVminsertURL)
	})

	t.Run("Verify", func(t *testing.T) {
		got, err := VerifyActionOptions(plan, dir, &schema.MigrationLoopAction{Kind: schema.ActionVerify, Manifest: "manifest.json"})
		require.NoError(t, err)
		assert.Equal(t, filepath.Join(dir, "manifest.json"), got.Manifest)
		assert.Equal(t, "http://origin", got.OriginVmselectURL)
		assert.Equal(t, "http://candidate", got.CandidateVmselectURL)
		assert.Equal(t, 45, got.TimeoutSeconds)
		assert.Equal(t, 0.02, got.Tolerance)
	})
}

func TestValidateRejectsMissingAndUnknownAction(t *testing.T) {
	plan := &schema.MigrationLoopPlan{Runtime: &schema.MigrationLoopRuntime{}, Vmselect: &schema.MigrationLoopVmselect{}}
	for _, tc := range []struct {
		name   string
		action *schema.MigrationLoopAction
	}{
		{name: "Missing", action: nil},
		{name: "Unknown", action: &schema.MigrationLoopAction{Kind: "shell"}},
	} {
		t.Run(tc.name, func(t *testing.T) {
			assert.Error(t, Validate(plan, tc.action))
		})
	}
}

func TestRunActionManifest(t *testing.T) {
	dir := t.TempDir()
	writeTenantsFile(t, dir)
	var progress bytes.Buffer
	err := RunAction(t.Context(), actionTestPlan(), dir, &schema.MigrationLoopAction{
		Kind:        schema.ActionManifest,
		TenantsFile: "tenants-all.json",
		Manifest:    "manifest.json",
	}, nil, ActionOutput{Progress: &progress})
	require.NoError(t, err)
	manifestPath := filepath.Join(dir, "manifest.json")
	assert.FileExists(t, manifestPath)
	assert.FileExists(t, status.PathForManifest(manifestPath))
	assert.Contains(t, progress.String(), "wrote "+filepath.Join(dir, "manifest.json"))
}

func TestRunActionMigrate(t *testing.T) {
	dir := t.TempDir()
	manifestPath := writeMigrationManifestFixture(t, dir)
	action := &schema.MigrationLoopAction{
		Kind:         schema.ActionMigrate,
		Manifest:     filepath.Base(manifestPath),
		StepInterval: "day",
	}

	t.Run("NilRunner", func(t *testing.T) {
		err := RunAction(t.Context(), actionTestPlan(), dir, action, nil, ActionOutput{})
		assert.ErrorContains(t, err, "no vmctl runner")
	})

	t.Run("Dispatch", func(t *testing.T) {
		runner := &recordingRunner{behavior: func([]string) (int, string, int64) {
			return 0, "total bytes: 1 KB\n", 1000
		}}
		var progress bytes.Buffer
		err := RunAction(t.Context(), actionTestPlan(), dir, action, runner, ActionOutput{Progress: &progress})
		require.NoError(t, err)
		assert.Len(t, runner.calls, 2)
		assert.Contains(t, progress.String(), "migration done")
	})
}

func TestRunActionVerify(t *testing.T) {
	var queries atomic.Int32
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if err := r.ParseForm(); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		if r.FormValue("query") == "" {
			http.Error(w, "missing query", http.StatusBadRequest)
			return
		}
		queries.Add(1)
		_, _ = w.Write([]byte(`{"status":"success","data":{"resultType":"scalar","result":[1,"10"]}}`))
	}))
	t.Cleanup(srv.Close)

	dir := t.TempDir()
	manifestPath := writeMigrationManifestFixture(t, dir)
	manifest, err := status.LoadManifest(manifestPath)
	require.NoError(t, err)
	doc := status.BuildInitialStatus(manifest, manifestPath)
	for i := range doc.Shards {
		doc.Shards[i].Phase = schema.PhaseImported
	}
	require.NoError(t, status.WriteJSONAtomic(status.PathForManifest(manifestPath), doc))

	plan := actionTestPlan()
	plan.Vmselect.Origin = srv.URL + "/select/{tenant}"
	plan.Vmselect.Candidate = srv.URL + "/select/{tenant}"
	var progress bytes.Buffer
	err = RunAction(t.Context(), plan, dir, &schema.MigrationLoopAction{
		Kind:     schema.ActionVerify,
		Manifest: filepath.Base(manifestPath),
	}, nil, ActionOutput{Progress: &progress})
	require.NoError(t, err)
	assert.Positive(t, queries.Load())
	assert.Contains(t, progress.String(), "verified=2")

	got, err := status.LoadDocument(status.PathForManifest(manifestPath))
	require.NoError(t, err)
	for _, shard := range got.Shards {
		assert.True(t, shard.Verified)
	}
}

func TestRunActionMissingArtifacts(t *testing.T) {
	dir := t.TempDir()
	for _, tc := range []struct {
		name   string
		action *schema.MigrationLoopAction
	}{
		{
			name: "ManifestTenants",
			action: &schema.MigrationLoopAction{
				Kind:        schema.ActionManifest,
				TenantsFile: "missing-tenants.json",
				Manifest:    "manifest.json",
			},
		},
		{
			name:   "VerifyManifest",
			action: &schema.MigrationLoopAction{Kind: schema.ActionVerify, Manifest: "missing-manifest.json"},
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			err := RunAction(t.Context(), actionTestPlan(), dir, tc.action, nil, ActionOutput{})
			assert.ErrorIs(t, err, os.ErrNotExist)
		})
	}
}
