package server

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"

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

func newProbeMigration(t *testing.T) *Migration {
	t.Helper()
	m := newTestMigration(t)
	m.plan = &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-17T00:00:00Z",
				StepInterval: "1d",
			},
			Verification: schema.MigrationLoopVerification{TimeoutSeconds: 60, Tolerance: 0.01},
		},
		Vmselect: &schema.MigrationLoopVmselect{
			Origin:    "http://origin",
			Migration: "http://migration",
			Candidate: "http://candidate",
		},
		Tenants: []schema.MigrationLoopTenant{
			{
				Name:        "t1",
				TenantsFile: "t1.json",
				Cmds: []schema.MigrationLoopCmd{{
					Step: "manifest",
					Action: &schema.MigrationLoopAction{
						Kind:        schema.ActionManifest,
						TenantsFile: "t1.json",
						Manifest:    "t1-manifest.json",
					},
				}},
			},
		},
	}
	return m
}

func startProbeServer(t *testing.T, body string) *httptest.Server {
	t.Helper()
	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
		fmt.Fprint(w, body)
	}))
}

func writeManifestFor(t *testing.T, dir, tenant string, shards int) {
	t.Helper()
	manifest := schema.Manifest{
		Tenants: []schema.ManifestTenant{{
			Tenant:          tenant,
			SourceAddr:      "x",
			DestinationAddr: "y",
		}},
	}
	for i := range shards {
		manifest.Tenants[0].Shards = append(manifest.Tenants[0].Shards, schema.ManifestShard{
			ShardID:   fmt.Sprintf("%s:%d", tenant, i),
			TimeStart: "2026-06-16T00:00:00Z",
			TimeEnd:   "2026-06-17T00:00:00Z",
		})
	}
	data, _ := json.MarshalIndent(manifest, "", "  ")
	path := filepath.Join(dir, tenant+"-manifest.json")
	assert.NoError(t, os.WriteFile(path, data, 0o644))
}

// newProbeCase starts a loopback vmselect serving body and builds the
// probe job targeting it. Server and migration dir are released on test
// cleanup.
func newProbeCase(t *testing.T, body string) (*Migration, *probeJob) {
	t.Helper()
	srv := startProbeServer(t, body)
	t.Cleanup(srv.Close)
	m := newProbeMigration(t)
	m.plan.Vmselect.Migration = srv.URL + "/select/{tenant}/prometheus"
	return m, &probeJob{m: m, client: srv.Client(), srcURL: m.plan.Vmselect.Migration}
}

func TestProbeJob(t *testing.T) {
	t.Run("GeneratesMissingManifestInlineWithoutExecutingCmd", func(t *testing.T) {
		m, job := newProbeCase(t, "abc")
		assert.NoError(t, os.WriteFile(filepath.Join(m.dir, "t1.json"), []byte(`["t1"]`), 0o644))

		assert.NoError(t, job.Run(context.Background()))
		_, err := os.Stat(filepath.Join(m.dir, "t1-manifest.json"))
		assert.NoError(t, err, "inline manifest not written")
		_, err = os.Stat(filepath.Join(m.dir, "t1-status.json"))
		assert.NoError(t, err, "inline manifest status not initialized")
		assert.Len(t, job.artifact.Tenants, 1, "artifact tenants=%d", len(job.artifact.Tenants))
	})

	t.Run("UsesManifestPathFromTypedAction", func(t *testing.T) {
		m, job := newProbeCase(t, "abc")
		tenant := &m.plan.Tenants[0]
		tenant.Name = "1001:0"
		tenant.TenantsFile = "tenants-1001-0.json"
		tenant.Cmds[0].Action.TenantsFile = tenant.TenantsFile
		tenant.Cmds[0].Action.Manifest = "tenants-1001-0-manifest.json"
		assert.NoError(t, os.WriteFile(filepath.Join(m.dir, tenant.TenantsFile), []byte(`["1001:0"]`), 0o644))

		assert.NoError(t, job.Run(context.Background()))
		_, err := os.Stat(filepath.Join(m.dir, tenant.Cmds[0].Action.Manifest))
		assert.NoError(t, err, "action manifest not used")
	})

	t.Run("AllTenantsManifestsExist", func(t *testing.T) {
		m, job := newProbeCase(t, strings.Repeat("x", 1024))
		writeManifestFor(t, m.dir, "t1", 2)
		startJob(t, m, job)
		waitMigrationIdle(t, m, 5*time.Second)
		assert.Len(t, job.artifact.Tenants, 1)
		assert.Len(t, job.artifact.Tenants[0].Shards, 2)
		assert.Equal(t, int64(2*1024), job.artifact.Totals.Bytes)
	})

	t.Run("NoVmselectBlock", func(t *testing.T) {
		m := newProbeMigration(t)
		m.plan.Vmselect = nil
		job := &probeJob{m: m, client: http.DefaultClient}
		startJob(t, m, job)
		waitMigrationIdle(t, m, 5*time.Second)
		st := m.Status()
		assert.Contains(t, st.LastResult, "vmselect block", "LastResult = %q, want vmselect block error", st.LastResult)
	})

	t.Run("DoesNotWriteStatusJSON", func(t *testing.T) {
		m, job := newProbeCase(t, "abc")
		writeManifestFor(t, m.dir, "t1", 1)

		startJob(t, m, job)
		waitMigrationIdle(t, m, 5*time.Second)
		matches, _ := filepath.Glob(filepath.Join(m.dir, "*-status.json"))
		assert.Empty(t, matches, "probe wrote status.json: %v", matches)
	})

	t.Run("WritesArtifact", func(t *testing.T) {
		m, job := newProbeCase(t, "abc")
		writeManifestFor(t, m.dir, "t1", 1)

		startJob(t, m, job)
		waitMigrationIdle(t, m, 5*time.Second)
		st := m.Status()
		got, err := m.JobArtifact(st.LastJobId)
		assert.NoError(t, err)
		var art probeArtifact
		assert.NoError(t, json.Unmarshal(got, &art), "parse")
		assert.Equal(t, "export-probe", art.Kind)
		assert.Len(t, art.Tenants, 1)
	})

	t.Run("PreCancelledContext", func(t *testing.T) {
		m, job := newProbeCase(t, "abc")
		writeManifestFor(t, m.dir, "t1", 1)

		startJob(t, m, job)
		assert.NoError(t, m.Cancel(m.Status().JobId))
		waitMigrationIdle(t, m, 5*time.Second)
		st := m.Status()
		assert.NotEmpty(t, st.LastResult, "LastResult empty; want ctx cancellation")
	})
}

func TestRenderProbeText(t *testing.T) {
	a := &probeArtifact{
		ID:         "export-probe-1",
		Kind:       "export-probe",
		StartedAt:  "2026-08-21T10:00:00Z",
		FinishedAt: "2026-08-21T10:01:00Z",
		Tenants: []probeTenantResult{{
			Tenant: "1001:0",
			Shards: []probeShardRow{
				{
					ShardID:       "1001:0:1",
					Window:        "2026-08-01T00:00:00Z - 2026-08-02T00:00:00Z",
					Bytes:         2048,
					DurMs:         34200,
					ThroughputBps: 59,
				},
				{
					ShardID: "1001:0:2",
					Window:  "2026-08-02T00:00:00Z - 2026-08-02T12:00:00Z",
					Bytes:   1024,
					DurMs:   500,
					Error:   "vmselect timeout after 60s: connection refused by peer 10.0.0.1:8481",
				},
			},
		}},
	}
	a.computeTotals()

	var buf strings.Builder
	renderProbeText(&buf, a)
	out := buf.String()

	assert.Contains(t, out, "job=export-probe-1 finished=2026-08-21T10:01:00Z")
	assert.Contains(t, out, "1001:0:1")
	assert.Contains(t, out, "2026-08-01T00:00:00Z - 2026-08-02T00:00:00Z", "window renders verbatim")
	assert.Contains(t, out, "2026-08-02T00:00:00Z - 2026-08-02T12:00:00Z")
	assert.Contains(t, out, "2.00K")
	assert.Contains(t, out, "34.2s")
	assert.Contains(t, out, "500ms")
	assert.Contains(t, out, "59B/s")
	assert.Contains(t, out, "error", "failed shard adds the error column")
	assert.NotContains(t, out, "8481", "long error truncates")
	assert.Contains(t, out, "total")
	assert.Contains(t, out, "2 shards")
	assert.Contains(t, out, "3.00K")
	assert.Contains(t, out, "34.7s")

	a.Tenants[0].Shards[1].Error = ""
	a.computeTotals()
	buf.Reset()
	renderProbeText(&buf, a)
	assert.NotContains(t, buf.String(), "error", "no failures means no error column")
}
