package devops

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"sync/atomic"
	"testing"
	"time"

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

// tenantServer serves one canned tenant response, recording what it was asked
// for.
func tenantServer(t *testing.T, body string) (*httptest.Server, *atomic.Int32, *http.Header, *string) {
	t.Helper()
	var calls atomic.Int32
	var lastHeader http.Header
	var lastPath string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		calls.Add(1)
		lastHeader = r.Header.Clone()
		lastPath = r.URL.Path
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprint(w, body)
	}))
	t.Cleanup(srv.Close)
	return srv, &calls, &lastHeader, &lastPath
}

// newTestClient is the in-cluster shape: an address and no credential, because
// the internal route authenticates nothing.
func newTestClient(t *testing.T, baseURL string) *Client {
	t.Helper()
	c, err := New(Config{BaseURL: baseURL}, nil)
	require.NoError(t, err)
	return c
}

func TestGetTenantReturnsCommercialProfile(t *testing.T) {
	srv, calls, header, path := tenantServer(t, `{
		"errcode": null,
		"id": "1372813089190491281",
		"name": " ACME Corp ",
		"kind": "External",
		"region": "NA",
		"segmentation": "KA",
		"email": "ops@acme.example"
	}`)
	c := newTestClient(t, srv.URL)

	got, err := c.GetTenant(context.Background(), " 1372813089190491281 ")
	require.NoError(t, err)
	require.Equal(t, Tenant{BizRegion: "NA", Kind: "external", Segmentation: "KA", Name: "ACME Corp"}, got)
	require.Equal(t, int32(1), calls.Load())
	// The internal route: no auth middleware, private network only.
	require.Equal(t, "/api/v1/devops-internal/tenants/1372813089190491281", *path)
	require.Empty(t, header.Get("X-API-TOKEN"))
	require.Empty(t, header.Get("X-API-KEY"))
}

// A token is only for reaching the authenticated listener through the gateway
// from outside the cluster; in-cluster there is none to send.
func TestGetTenantSendsAConfiguredToken(t *testing.T) {
	srv, _, header, _ := tenantServer(t, `{"kind": "external"}`)
	c, err := New(Config{BaseURL: srv.URL, APIToken: "token"}, nil)
	require.NoError(t, err)

	_, err = c.GetTenant(context.Background(), "42")
	require.NoError(t, err)
	require.Equal(t, "token", header.Get("X-API-TOKEN"))
	require.Equal(t, "token", header.Get("X-API-KEY"))
}

// Kind is lowercased on the way in because jiraport compares it
// case-insensitively while alertstore's own filters compare it exactly: one
// spelling stored is what keeps `tenant_kind=internal` from missing the alerts
// whose response said `Internal`.
func TestGetTenantNormalizesKind(t *testing.T) {
	srv, _, _, _ := tenantServer(t, `{"kind": " INTERNAL ", "region": " APAC ", "segmentation": " SMB "}`)
	c := newTestClient(t, srv.URL)

	got, err := c.GetTenant(context.Background(), "42")
	require.NoError(t, err)
	require.Equal(t, "internal", got.Kind)
	// Region and segmentation keep the portal's casing; only kind is folded down.
	require.Equal(t, "APAC", got.BizRegion)
	require.Equal(t, "SMB", got.Segmentation)
}

func TestGetTenantCachesByTenantID(t *testing.T) {
	srv, calls, _, _ := tenantServer(t, `{"kind": "external", "region": "EMEA"}`)
	c := newTestClient(t, srv.URL)

	for range 3 {
		got, err := c.GetTenant(context.Background(), "42")
		require.NoError(t, err)
		require.Equal(t, "EMEA", got.BizRegion)
	}
	require.Equal(t, int32(1), calls.Load())

	// A different tenant is a different question.
	_, err := c.GetTenant(context.Background(), "43")
	require.NoError(t, err)
	require.Equal(t, int32(2), calls.Load())
}

func TestGetTenantRefetchesAfterTTL(t *testing.T) {
	srv, calls, _, _ := tenantServer(t, `{"kind": "external", "region": "EMEA"}`)
	c, err := New(Config{BaseURL: srv.URL, CacheTTL: time.Nanosecond}, nil)
	require.NoError(t, err)

	_, err = c.GetTenant(context.Background(), "42")
	require.NoError(t, err)
	time.Sleep(time.Millisecond)
	_, err = c.GetTenant(context.Background(), "42")
	require.NoError(t, err)
	require.Equal(t, int32(2), calls.Load())
}

// An unknown tenant is an empty record, not a failure — and it is cached, so a
// storm on a deleted account does not become a request per occurrence.
func TestGetTenantAcceptsAnEmptyRecord(t *testing.T) {
	srv, calls, _, _ := tenantServer(t, `{"errcode": null}`)
	c := newTestClient(t, srv.URL)

	got, err := c.GetTenant(context.Background(), "42")
	require.NoError(t, err)
	require.Equal(t, Tenant{}, got)

	_, err = c.GetTenant(context.Background(), "42")
	require.NoError(t, err)
	require.Equal(t, int32(1), calls.Load())
}

// The ops portal numbers its tenants and rejects anything else with
// `id is not a int`. Alerts carry names in the tenant labels anyway — the
// fleet's own infrastructure has no account behind it — and asking about one
// would be a doomed request per occurrence, since failures are not cached.
func TestGetTenantAsksNothingWithoutANumericTenantID(t *testing.T) {
	srv, calls, _, _ := tenantServer(t, `{"kind": "external"}`)
	c := newTestClient(t, srv.URL)

	for _, tenantID := range []string{"", "  ", "infra-dev", "staging-devtier", "tidb123", "12 3"} {
		got, err := c.GetTenant(context.Background(), tenantID)
		require.NoError(t, err, tenantID)
		require.Equal(t, Tenant{}, got, tenantID)
	}
	require.Equal(t, int32(0), calls.Load())

	// A numbered tenant is still asked about, surrounding space and all.
	_, err := c.GetTenant(context.Background(), " 42 ")
	require.NoError(t, err)
	require.Equal(t, int32(1), calls.Load())
}

func TestGetTenantReportsHTTPFailure(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusForbidden)
		fmt.Fprint(w, `{"errcode": "PermissionDenied"}`)
	}))
	t.Cleanup(srv.Close)
	c := newTestClient(t, srv.URL)

	_, err := c.GetTenant(context.Background(), "42")
	require.ErrorContains(t, err, "HTTP 403")
	require.ErrorContains(t, err, "PermissionDenied")
}

// Only the address decides. Requiring a token would disable enrichment in the
// in-cluster deployment this client is written for, where there is none.
func TestDisabledWithoutABaseURL(t *testing.T) {
	c, err := New(Config{APIToken: "token"}, nil)
	require.NoError(t, err)
	require.False(t, c.Enabled())

	_, err = c.GetTenant(context.Background(), "42")
	require.ErrorIs(t, err, ErrDisabled)
}

func TestEnabledWithABaseURLAlone(t *testing.T) {
	c, err := New(Config{BaseURL: "http://devops-server.aisupport.svc.cluster.local:8080"}, nil)
	require.NoError(t, err)
	require.True(t, c.Enabled())
}

func TestNewRejectsAnUnusableBaseURL(t *testing.T) {
	_, err := New(Config{BaseURL: "prod.devops.pingcap.com"}, nil)
	require.ErrorContains(t, err, "invalid devops base_url")
}

// A nil client is what a caller holds when the integration was never
// constructed; it must answer disabled rather than panic.
func TestNilClientIsDisabled(t *testing.T) {
	var c *Client
	require.False(t, c.Enabled())
}
