package vmquery

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"net"
	"net/http"
	"net/http/httptest"
	"syscall"
	"testing"
	"time"

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

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

func TestEndpointStripsTrailingSlash(t *testing.T) {
	f := func(base, api, want string) {
		t.Helper()
		assert.Equal(t, want, Endpoint(base, api), "base=%q api=%q", base, api)
	}
	f("http://v/", "/api/v1/query", "http://v/api/v1/query")
	f("http://v", "/api/v1/query", "http://v/api/v1/query")
	f("http://v/select/1001:0/prometheus", "/api/v1/query", "http://v/select/1001:0/prometheus/api/v1/query")
}

func TestRenderURLPreservesColon(t *testing.T) {
	f := func(template, tenant, want string) {
		t.Helper()
		assert.Equal(t, want, RenderURL(template, tenant), "template=%q tenant=%q", template, tenant)
	}
	f("http://v/select/{tenant}/prometheus", "1001:0", "http://v/select/1001:0/prometheus")
	f("http://v/select/{tenant}/prometheus", "1001:42", "http://v/select/1001:42/prometheus")
	f("http://v/select/{tenant}/prometheus", "weird:tenant:id", "http://v/select/weird:tenant:id/prometheus")
	f("http://v/{tenant}", "a b", "http://v/a%20b")
}

func mustDecode(t *testing.T, body string) *QueryResponse {
	t.Helper()
	var r QueryResponse
	require.NoError(t, json.Unmarshal([]byte(body), &r))
	return &r
}

func TestResponseValuesMatrix(t *testing.T) {
	r := mustDecode(t, `{
		  "status": "success",
		  "data": {
		    "resultType": "matrix",
		    "result": [
		      {"values": [[1, "10"], [2, "20"]]},
		      {"values": [[3, "5"]]}
		    ]
		  }
		}`)
	values, err := r.Values()
	require.NoError(t, err)
	assert.Equal(t, []float64{10, 20, 5}, values)
}

func TestResponseValuesVector(t *testing.T) {
	r := mustDecode(t, `{
		  "status": "success",
		  "data": {
		    "resultType": "vector",
		    "result": [{"value": [1, "42"]}, {"value": [2, "8"]}]
		  }
		}`)
	values, err := r.Values()
	require.NoError(t, err)
	assert.Equal(t, []float64{42, 8}, values)
}

func TestResponseValuesScalar(t *testing.T) {
	r := mustDecode(t, `{"status":"success","data":{"resultType":"scalar","result":[1234, "7"]}}`)
	values, err := r.Values()
	require.NoError(t, err)
	assert.Equal(t, []float64{7}, values)
}

func TestResponseValuesErrorStatus(t *testing.T) {
	r := mustDecode(t, `{"status":"error","errorType":"bad_data","error":"invalid query"}`)
	err := r.Err()
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "invalid query")
	_, err = r.Values()
	assert.Error(t, err)
}

func TestResponseCountAndMax(t *testing.T) {
	r := mustDecode(t, `{
		  "status": "success",
		  "data": {
		    "resultType": "vector",
		    "result": [{"value": [1, "10"]}, {"value": [2, "20"]}, {"value": [3, "5"]}]
		  }
		}`)
	sum, err := r.Count()
	require.NoError(t, err)
	assert.Equal(t, float64(35), sum)
}

func TestResponseScalarBareNumber(t *testing.T) {
	r := mustDecode(t, `{"status":"success","data":{"resultType":"scalar","result":[1234, 7.5]}}`)
	values, err := r.Values()
	require.NoError(t, err)
	assert.Equal(t, []float64{7.5}, values)
}

func TestResponseScalarMalformedValue(t *testing.T) {
	r := mustDecode(t, `{"status":"success","data":{"resultType":"scalar","result":[1234, "not-a-number"]}}`)
	_, err := r.Values()
	assert.Error(t, err)
}

func TestHTTPClientQuerySuccess(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		assert.Equal(t, http.MethodPost, r.Method)
		assert.Equal(t, "application/x-www-form-urlencoded", r.Header.Get("Content-Type"))
		if err := r.ParseForm(); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		assert.Equal(t, "up", r.FormValue("query"))
		w.Header().Set("Content-Type", "application/json")
		w.Write([]byte(`{"status":"success","data":{"resultType":"scalar","result":[1, "7"]}}`))
	}))
	defer srv.Close()

	c := NewHTTPClient()
	resp, err := c.Query(context.Background(), srv.URL, "/api/v1/query",
		map[string]string{"query": "up"}, time.Second)
	require.NoError(t, err)
	assert.Equal(t, "success", resp.Status)
	values, err := resp.Values()
	require.NoError(t, err)
	assert.Equal(t, []float64{7}, values)
}

func TestHTTPClientQueryHTTPError(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusBadRequest)
		w.Write([]byte(`{"status":"error","error":"bad request"}`))
	}))
	defer srv.Close()

	c := NewHTTPClient()
	_, err := c.Query(context.Background(), srv.URL, "/api/v1/query",
		map[string]string{"query": "up"}, time.Second)
	assert.Error(t, err)
}

func TestHTTPErrorRetryClassification(t *testing.T) {
	cases := []struct {
		name      string
		code      int
		transient bool
		permanent bool
	}{
		{"500 is transient", 500, true, false},
		{"503 is transient", 503, true, false},
		{"499 is permanent", 499, false, true},
		{"404 is permanent", 404, false, true},
		{"400 is permanent", 400, false, true},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := &HTTPError{Code: tc.code, Status: "test"}
			assert.Equal(t, tc.transient, errors.Is(err, retry.ErrTransient))
			assert.Equal(t, tc.permanent, errors.Is(err, retry.ErrPermanent))
		})
	}
}

type FakeClient struct {
	OnQuery func(ctx context.Context, baseURL, api string, params map[string]string, timeout time.Duration) (*QueryResponse, error)
}

func (f *FakeClient) Query(ctx context.Context, baseURL, api string, params map[string]string, timeout time.Duration) (*QueryResponse, error) {
	return f.OnQuery(ctx, baseURL, api, params, timeout)
}

type timeoutNetErr struct{ msg string }

func (e *timeoutNetErr) Error() string   { return e.msg }
func (e *timeoutNetErr) Timeout() bool   { return true }
func (e *timeoutNetErr) Temporary() bool { return false }

type nonTimeoutNetErr struct{ msg string }

func (e *nonTimeoutNetErr) Error() string   { return e.msg }
func (e *nonTimeoutNetErr) Timeout() bool   { return false }
func (e *nonTimeoutNetErr) Temporary() bool { return true }

func TestWrapConnErrorNil(t *testing.T) {
	assert.Nil(t, wrapConnError(nil))
}

func TestWrapConnErrorTimeoutNotWrapped(t *testing.T) {
	e := &timeoutNetErr{msg: "i/o timeout"}
	assert.Same(t, e, wrapConnError(e))
}

func TestWrapConnErrorNonTimeoutNetErrWrapped(t *testing.T) {
	e := &nonTimeoutNetErr{msg: "connection reset by peer"}
	got := wrapConnError(e)
	var ce *ConnError
	require.ErrorAs(t, got, &ce)
	assert.Same(t, e, ce.Inner)
	assert.True(t, errors.Is(ce, retry.ErrTransient))
}

func TestWrapConnErrorRefusedSyscall(t *testing.T) {
	op := &net.OpError{Op: "dial", Net: "tcp", Err: syscall.ECONNREFUSED}
	got := wrapConnError(op)
	var ce *ConnError
	require.ErrorAs(t, got, &ce)
	assert.True(t, errors.Is(ce, syscall.ECONNREFUSED))
}

func TestWrapConnErrorEOF(t *testing.T) {
	got := wrapConnError(io.EOF)
	var ce *ConnError
	assert.True(t, errors.As(got, &ce))
}

func TestWrapConnErrorUnrelatedErrPassthrough(t *testing.T) {
	e := errors.New("some non-network error")
	assert.Same(t, e, wrapConnError(e))
}

func TestNewHTTPClientDefaultPolicy(t *testing.T) {
	def := NewHTTPClient()
	assert.Equal(t, 2, def.RetryPolicy.MaxAttempts)
	assert.Nil(t, def.RetryPolicy.Backoff, "default query policy must not back off")
}

func TestErrorMessages(t *testing.T) {
	assert.Equal(t, "vmquery: HTTP 503 Service Unavailable",
		(&HTTPError{Status: "503 Service Unavailable"}).Error())
	inner := errors.New("reset")
	assert.Equal(t, "vmquery: connection error: reset",
		(&ConnError{Inner: inner}).Error())
	assert.Equal(t, inner, errors.Unwrap(&ConnError{Inner: inner}))
}

func TestQueryResponseErrVariants(t *testing.T) {
	var nilResp *QueryResponse
	assert.EqualError(t, nilResp.Err(), "vmquery: nil response")

	r := &QueryResponse{Status: "error", ErrorType: "bad_data"}
	assert.EqualError(t, r.Err(), "bad_data")

	r = &QueryResponse{Status: "error"}
	assert.EqualError(t, r.Err(), "vmquery: query failed")

	r = &QueryResponse{Status: "success"}
	require.NoError(t, r.Err())
}

func TestResponseValuesMatrixEdgeCases(t *testing.T) {
	r := mustDecode(t, `{"status":"success","data":{"resultType":"matrix","result":null}}`)
	values, err := r.Values()
	require.NoError(t, err)
	assert.Empty(t, values)

	r = mustDecode(t, `{"status":"success","data":{"resultType":"matrix","result":{"not":"a series array"}}}`)
	_, err = r.Values()
	assert.ErrorContains(t, err, "decode series")
}

func TestPersistFailure(t *testing.T) {
	ctxErr := persistFailure("s1", context.Canceled)
	assert.ErrorIs(t, ctxErr, context.Canceled, "ctx cancellation must pass through as shutdown, not failure")

	err := persistFailure("s1", errors.New("disk full"))
	var ee *exitcode.Error
	require.ErrorAs(t, err, &ee)
	assert.Equal(t, exitcode.Partial, ee.Code)
	assert.ErrorContains(t, err, "persist status for shard s1")
}
