package cloudproviderhandler

import (
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"

	"github.com/pingcap/dbaas/billing2/config"
)

// TestGenRequestUrl_BackUp_NoDoubleFilter is a regression test for the production
// incident at 2026-07-09 02:28 UTC where the BackUp branch of genRequestUrl
// prepended "?$filter=" to apiFilters. The caller then injected it again via
// q.Set("$filter", apiFilters), producing ...$filter=?$filter=... which Azure
// rejected with HTTP 400, and after 3 retries the missing err caused a
// nil-pointer panic in GetPrice.
func TestGenRequestUrl_BackUp_NoDoubleFilter(t *testing.T) {
	config.Get().Set(config.AzureSkuPriceQueryUrl, "https://prices.azure.com/api/retail/prices")

	got, err := genRequestUrl(map[string]string{
		"service_code": BackUp,
		"region":       "japaneast",
	})
	if err != nil {
		t.Fatalf("genRequestUrl returned err: %v", err)
	}

	// 1. The resulting URL must not contain a literal "?$filter=" inside any
	//    query value — that is the exact substring that caused Azure to 400.
	if strings.Contains(got, "?$filter=") {
		t.Fatalf("URL contains literal '?$filter=' which Azure rejects: %s", got)
	}
	if strings.Contains(got, "%3F%24filter%3D") {
		t.Fatalf("URL contains url-encoded '%%3F%%24filter%%3D' which Azure rejects: %s", got)
	}

	// 2. The $filter query parameter must appear exactly once.
	u, err := url.Parse(got)
	if err != nil {
		t.Fatalf("URL parse failed: %v", err)
	}
	filters := u.Query()["$filter"]
	if len(filters) != 1 {
		t.Fatalf("expected exactly one $filter query param, got %d: %v", len(filters), filters)
	}

	// 3. The filter value itself must start with the OData expression, not
	//    with a stray '?'.
	if strings.HasPrefix(filters[0], "?") {
		t.Fatalf("$filter value starts with '?' — malformed: %s", filters[0])
	}

	// 4. The expected meterName / productName / region must be in the filter.
	for _, want := range []string{
		"serviceFamily eq 'Storage'",
		"meterName eq 'Hot LRS Data Stored'",
		"productName eq 'General Block Blob v2 Hierarchical Namespace'",
		"armRegionName eq 'japaneast'",
	} {
		if !strings.Contains(filters[0], want) {
			t.Errorf("$filter missing %q\n  got: %s", want, filters[0])
		}
	}
}

// TestSendRequestWithRetry_HTTPErrorNotNilNil guards against the historical
// behavior where, if every retry returned a non-200 HTTP response, the loop
// ended with response != nil and err == nil, and the function then returned
// `nil, err` — discarding the response while leaving the caller no signal that
// the request had failed at all. The current contract is: any unsuccessful
// final state must surface as a non-nil error.
func TestSendRequestWithRetry_HTTPErrorNotNilNil(t *testing.T) {
	var attempts int
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		attempts++
		// Always answer 400 — the exact failure mode we saw in production for
		// the BackUp URL.
		w.WriteHeader(http.StatusBadRequest)
		_, _ = w.Write([]byte(`{"error":"bad request"}`))
	}))
	defer srv.Close()

	resp, err := sendRequestWithRetry(http.DefaultClient, srv.URL, 3)

	if err == nil {
		t.Fatalf("expected non-nil error after 3 HTTP-400 attempts; got resp=%v err=nil", resp)
	}
	if resp == nil {
		t.Fatalf("expected non-nil response so caller can read body for diagnostics; got nil")
	}
	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("expected status %d, got %d", http.StatusBadRequest, resp.StatusCode)
	}
	if attempts != 3 {
		t.Fatalf("expected exactly 3 attempts, got %d", attempts)
	}
	// Make sure the documented contract is in the error string.
	if !strings.Contains(err.Error(), "400") {
		t.Fatalf("error should carry HTTP status for diagnosis, got %q", err.Error())
	}
}

// TestSendRequestWithRetry_NetworkErrorSurfacesAsError exercises the all-fail
// transport path so we keep the (nil, err) return contract on network errors.
func TestSendRequestWithRetry_NetworkErrorSurfacesAsError(t *testing.T) {
	// An URL pointing at a closed port is the simplest way to guarantee a
	// transport-layer error on every retry.
	resp, err := sendRequestWithRetry(http.DefaultClient, "http://127.0.0.1:1/never", 2)
	if err == nil {
		t.Fatalf("expected error from closed-port URL, got resp=%v err=nil", resp)
	}
}

// TestGetPrice_NilResponseIsHandled is a defensive test: if a future change
// reintroduces the (nil, nil) return path in sendRequestWithRetry, GetPrice
// must convert that into an error rather than panic on the deferred Close.
func TestGetPrice_NilResponseIsHandled(t *testing.T) {
	// We can't easily mock sendRequestWithRetry without DI; instead, simulate
	// the broken contract end-to-end by pointing AzureSkuPriceQueryEnable=true,
	// config URL at a closed port (so every retry errors out at transport),
	// and asserting the result is an error — never a panic.
	config.Get().Set(config.AzureSkuPriceQueryEnable, true)
	config.Get().Set(config.AzureSkuPriceQueryUrl, "http://127.0.0.1:1")

	h := &AzurePriceHandlerImpl{}
	defer func() {
		if r := recover(); r != nil {
			t.Fatalf("GetPrice must not panic on transport failure, got: %v", r)
		}
	}()

	_, err := h.GetPrice(map[string]string{
		"service_code": NodeCompute,
		"region":       "eastus",
		"instanceType": "Standard_D2s_v5",
	})
	if err == nil {
		t.Fatalf("expected error from closed-port URL, got nil")
	}
	if !strings.Contains(err.Error(), "azure") {
		t.Logf("note: error type is %T (%v)", err, err)
	}
}
