package analytics_api_test

// Docs:
// - Dashboard Results (GET/PATCH/PUT/DELETE): https://developer.salesforce.com/docs/atlas.en-us.254.0.api_analytics.meta/api_analytics/analytics_api_dashboard_results_resource.htm
// - Save a Dashboard (PATCH example): https://developer.salesforce.com/docs/atlas.en-us.254.0.api_analytics.meta/api_analytics/analytics_api_dashboard_save_dashboard.htm
// - Refresh a Dashboard (PUT example): https://developer.salesforce.com/docs/atlas.en-us.254.0.api_analytics.meta/api_analytics/analytics_api_dashboard_refresh.htm
// - Delete a Dashboard (DELETE example): https://developer.salesforce.com/docs/atlas.en-us.254.0.api_analytics.meta/api_analytics/analytics_api_dashboard_delete.htm
// - Dashboard Status (GET): https://developer.salesforce.com/docs/atlas.en-us.254.0.api_analytics.meta/api_analytics/analytics_api_dashboard_status_resource.htm

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

	"github.com/nexuscrm/tests/contract/analytics_api/internal/arrange"
	"github.com/nexuscrm/tests/contract/internal/contracttest"
	"github.com/nexuscrm/tests/contract/internal/liveapi"
	"github.com/stretchr/testify/require"
)

func TestAnalyticsAPI_DashboardMutations(t *testing.T) {
	cfg := liveapi.LoadConfig(t)
	client := liveapi.NewAuthenticatedClient(t)

	// Arrange: dashboards require a folderId; pick one from the folders API.
	folderID := arrange.PickFolderID(t, client, "Dashboards")

	createReq := map[string]any{
		"name":        arrange.UniqueName("contract-dashboard-mutate"),
		"folderId":    folderID,
		"description": "contract test dashboard mutation",
	}

	createResp, err := client.DoJSON(http.MethodPost, "/analytics/dashboards", createReq)
	require.NoError(t, err)
	require.Contains(t, []int{http.StatusCreated, http.StatusOK}, createResp.StatusCode)

	var createBody map[string]any
	liveapi.DecodeJSON(t, createResp, &createBody)

	dashboardID := coalesceString(createBody, "id", "Id")
	require.NotEmpty(t, dashboardID, "create dashboard should return id")
	contracttest.AssertSalesforceID(t, dashboardID)

	// Cleanup best-effort delete (even if tests delete it explicitly later).
	t.Cleanup(func() {
		resp, err := client.Do(http.MethodDelete, "/analytics/dashboards/"+url.PathEscape(dashboardID), "", nil)
		if err == nil {
			_ = resp.Body.Close()
		}
	})

	t.Run("unauthorized requests reject missing bearer token", func(t *testing.T) {
		cases := []struct {
			name   string
			method string
			path   string
			body   any
		}{
			{
				name:   "PATCH /analytics/dashboards/{id}",
				method: http.MethodPatch,
				path:   "/analytics/dashboards/" + dashboardID,
				body: map[string]any{
					"dashboardMetadata": map[string]any{"name": "unauthorized"},
				},
			},
			{
				name:   "PUT /analytics/dashboards/{id}",
				method: http.MethodPut,
				path:   "/analytics/dashboards/" + dashboardID,
			},
			{
				name:   "DELETE /analytics/dashboards/{id}",
				method: http.MethodDelete,
				path:   "/analytics/dashboards/" + dashboardID,
			},
			{
				name:   "POST /analytics/dashboards/{id}/refresh",
				method: http.MethodPost,
				path:   "/analytics/dashboards/" + dashboardID + "/refresh",
			},
			{
				name:   "GET /analytics/dashboards/{id}/status",
				method: http.MethodGet,
				path:   "/analytics/dashboards/" + dashboardID + "/status",
			},
		}

		for _, tc := range cases {
			t.Run(tc.name, func(t *testing.T) {
				resp := contracttest.DoAnonymousRequest(t, tc.method, tc.path, tc.body)
				contracttest.AssertSalesforceError(t, resp, http.StatusUnauthorized, "INVALID_SESSION_ID")
			})
		}
	})

	t.Run("patch saves dashboard metadata (name)", func(t *testing.T) {
		updatedName := arrange.UniqueName("updated-dashboard")

		resp, err := client.DoJSON(http.MethodPatch, "/analytics/dashboards/"+url.PathEscape(dashboardID), map[string]any{
			"dashboardMetadata": map[string]any{
				"name": updatedName,
			},
		})
		require.NoError(t, err)
		require.Equal(t, http.StatusOK, resp.StatusCode)
		contracttest.AssertJSONContentType(t, resp)
		_ = resp.Body.Close()

		// Verify via GET results: status codes and payload shape can differ between sandbox and local.
		getResp, err := client.DoJSON(http.MethodGet, "/analytics/dashboards/"+url.PathEscape(dashboardID), nil)
		require.NoError(t, err)
		require.Equal(t, http.StatusOK, getResp.StatusCode)

		var body map[string]any
		liveapi.DecodeJSON(t, getResp, &body)

		if meta, ok := body["dashboardMetadata"].(map[string]any); ok {
			if name, ok := meta["name"].(string); ok && name != "" {
				require.Equal(t, updatedName, name)
			}
		}
	})

	t.Run("put triggers refresh and returns a status url", func(t *testing.T) {
		resp, err := client.DoJSON(http.MethodPut, "/analytics/dashboards/"+url.PathEscape(dashboardID), nil)
		require.NoError(t, err)
		require.Equal(t, http.StatusOK, resp.StatusCode)

		var body map[string]any
		liveapi.DecodeJSON(t, resp, &body)
		statusURL := coalesceString(body, "statusUrl", "statusURL")
		require.NotEmpty(t, statusURL)
	})

	t.Run("status returns componentStatus array", func(t *testing.T) {
		resp, err := client.DoJSON(http.MethodGet, "/analytics/dashboards/"+url.PathEscape(dashboardID)+"/status", nil)
		require.NoError(t, err)
		require.Equal(t, http.StatusOK, resp.StatusCode)
		contracttest.AssertJSONContentType(t, resp)

		var body map[string]any
		liveapi.DecodeJSON(t, resp, &body)

		// Salesforce returns componentStatus; local may return a compatible envelope.
		if _, ok := body["componentStatus"]; ok {
			return
		}
		require.NotEmpty(t, body)
	})

	t.Run("post refresh endpoint is target-dependent", func(t *testing.T) {
		resp, err := client.DoJSON(http.MethodPost, "/analytics/dashboards/"+url.PathEscape(dashboardID)+"/refresh", nil)
		require.NoError(t, err)

		if cfg.TargetName == "sandbox" && resp.StatusCode == http.StatusNotFound {
			// Salesforce docs describe refresh via PUT on the dashboard results resource.
			_ = resp.Body.Close()
			t.Skip("Salesforce sandbox does not expose POST /analytics/dashboards/{id}/refresh in this target")
		}

		require.Contains(t, []int{http.StatusOK, http.StatusAccepted}, resp.StatusCode)
		contracttest.AssertJSONContentType(t, resp)
		_ = resp.Body.Close()
	})

	t.Run("delete returns 204 and subsequent GET is not found", func(t *testing.T) {
		resp, err := client.Do(http.MethodDelete, "/analytics/dashboards/"+url.PathEscape(dashboardID), "", nil)
		require.NoError(t, err)
		require.Equal(t, http.StatusNoContent, resp.StatusCode)
		require.NoError(t, resp.Body.Close())

		afterResp, err := client.DoJSON(http.MethodGet, "/analytics/dashboards/"+url.PathEscape(dashboardID), nil)
		require.NoError(t, err)

		if cfg.TargetName == "sandbox" {
			arrange.RequireSalesforceErrorOneOf(t, afterResp, http.StatusNotFound)
			return
		}
		require.Contains(t, []int{http.StatusNotFound, http.StatusInternalServerError}, afterResp.StatusCode)
		_ = afterResp.Body.Close()
	})

	t.Run("not found dashboard IDs surface target specific errors", func(t *testing.T) {
		missingID := "01Z000000000000AAA"
		paths := []struct {
			method string
			path   string
			body   any
		}{
			{method: http.MethodPatch, path: "/analytics/dashboards/" + missingID, body: map[string]any{"dashboardMetadata": map[string]any{"name": "missing"}}},
			{method: http.MethodPut, path: "/analytics/dashboards/" + missingID},
			{method: http.MethodDelete, path: "/analytics/dashboards/" + missingID},
			{method: http.MethodPost, path: "/analytics/dashboards/" + missingID + "/refresh"},
			{method: http.MethodGet, path: "/analytics/dashboards/" + missingID + "/status"},
		}

		for _, tc := range paths {
			t.Run(tc.method+" "+tc.path, func(t *testing.T) {
				resp, err := client.DoJSON(tc.method, tc.path, tc.body)
				require.NoError(t, err)

				if cfg.TargetName == "sandbox" {
					arrange.RequireSalesforceErrorOneOf(t, resp, http.StatusNotFound)
					return
				}
				require.Contains(t, []int{http.StatusNotFound, http.StatusInternalServerError}, resp.StatusCode)
				_ = resp.Body.Close()
			})
		}
	})
}

func coalesceString(obj map[string]any, keys ...string) string {
	for _, key := range keys {
		if value, ok := obj[key]; ok {
			if s, ok := value.(string); ok && s != "" {
				return s
			}
		}
	}
	return ""
}
