package rest_api_test

// Docs:
// - Endpoint: https://developer.salesforce.com/docs/atlas.en-us.mobile_sdk.meta/mobile_sdk/oauth_using_identity_urls.htm
// - Request: https://developer.salesforce.com/docs/atlas.en-us.mobile_sdk.meta/mobile_sdk/oauth_using_identity_urls.htm
// - Response: https://developer.salesforce.com/docs/atlas.en-us.mobile_sdk.meta/mobile_sdk/oauth_using_identity_urls.htm
// - Sub-schema: https://developer.salesforce.com/docs/atlas.en-us.mobile_sdk.meta/mobile_sdk/oauth_using_identity_urls.htm
// - Errors: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_rest.htm

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

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

type identityFixture struct {
	client    *liveapi.Client
	targetURL string
}

type identityResponse struct {
	ID               string            `json:"id"`
	AssertedUser     bool              `json:"asserted_user"`
	UserID           string            `json:"user_id"`
	OrganizationID   string            `json:"organization_id"`
	Username         string            `json:"username"`
	NickName         string            `json:"nick_name"`
	DisplayName      string            `json:"display_name"`
	Email            string            `json:"email"`
	EmailVerified    bool              `json:"email_verified"`
	Photos           map[string]string `json:"photos"`
	URLs             map[string]string `json:"urls"`
	Active           bool              `json:"active"`
	UserType         string            `json:"user_type"`
	Language         string            `json:"language"`
	Locale           string            `json:"locale"`
	UTCOffset        int64             `json:"utcOffset"`
	LastModifiedDate string            `json:"last_modified_date"`
}

const (
	identityExplicitVersion = "63.0"
)

func TestGetIdentity(t *testing.T) {
	testCases := []struct {
		name    string
		arrange func(t *testing.T) identityFixture
		act     func(t *testing.T, fixture identityFixture) *http.Response
		assert  func(t *testing.T, fixture identityFixture, resp *http.Response)
	}{
		{
			name: "identity url returns placeholder api urls when version is omitted",
			// Doc: https://developer.salesforce.com/docs/atlas.en-us.mobile_sdk.meta/mobile_sdk/oauth_using_identity_urls.htm
			arrange: func(t *testing.T) identityFixture {
				contracttest.EnsureLocalRESTBaseline(t)
				client := liveapi.NewAuthenticatedClient(t)
				return identityFixture{
					client:    client,
					targetURL: discoverIdentityURL(t, client),
				}
			},
			act: func(t *testing.T, fixture identityFixture) *http.Response {
				resp, err := fixture.client.DoJSON(http.MethodGet, fixture.targetURL, nil)
				require.NoError(t, err)
				return resp
			},
			assert: func(t *testing.T, fixture identityFixture, resp *http.Response) {
				require.Equal(t, http.StatusOK, resp.StatusCode)

				var body identityResponse
				liveapi.DecodeJSON(t, resp, &body)

				assertBasicIdentityShape(t, body)
				assert.Equal(t, fixture.targetURL, body.ID)
				assertIdentityURLsForImplicitVersion(t, body.URLs)
			},
		},
		{
			name: "identity url respects explicit version parameter",
			// Doc: https://developer.salesforce.com/docs/atlas.en-us.mobile_sdk.meta/mobile_sdk/oauth_using_identity_urls.htm
			arrange: func(t *testing.T) identityFixture {
				contracttest.EnsureLocalRESTBaseline(t)
				client := liveapi.NewAuthenticatedClient(t)
				identityURL := discoverIdentityURL(t, client)
				return identityFixture{
					client:    client,
					targetURL: addIdentityVersionQuery(t, identityURL, identityExplicitVersion),
				}
			},
			act: func(t *testing.T, fixture identityFixture) *http.Response {
				resp, err := fixture.client.DoJSON(http.MethodGet, fixture.targetURL, nil)
				require.NoError(t, err)
				return resp
			},
			assert: func(t *testing.T, fixture identityFixture, resp *http.Response) {
				require.Equal(t, http.StatusOK, resp.StatusCode)

				var body identityResponse
				liveapi.DecodeJSON(t, resp, &body)

				assertBasicIdentityShape(t, body)
				assertIdentityURLsForExplicitVersion(t, body.URLs)
			},
		},
		{
			name: "missing bearer token is rejected",
			// Doc: https://developer.salesforce.com/docs/atlas.en-us.mobile_sdk.meta/mobile_sdk/oauth_using_identity_urls.htm
			arrange: func(t *testing.T) identityFixture {
				cfg := liveapi.LoadConfig(t)
				return identityFixture{
					targetURL: strings.TrimRight(cfg.RootBaseURL, "/") + "/id/00D000000000001/005000000000001AAA",
				}
			},
			act: func(t *testing.T, fixture identityFixture) *http.Response {
				return doAnonymousAbsoluteRequest(t, fixture.targetURL)
			},
			assert: func(t *testing.T, fixture identityFixture, resp *http.Response) {
				require.Equal(t, http.StatusForbidden, resp.StatusCode)
				body := string(liveapi.ReadBody(t, resp))
				assert.Contains(t, body, "Missing_OAuth_Token")
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			fixture := tc.arrange(t)
			resp := tc.act(t, fixture)
			tc.assert(t, fixture, resp)
		})
	}
}

func discoverIdentityURL(t *testing.T, client *liveapi.Client) string {
	t.Helper()

	resp, err := client.DoJSON(http.MethodGet, "/", nil)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.StatusCode)

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

	identityURL := resources["identity"]
	require.NotEmpty(t, identityURL, "expected identity url in resources-by-version response")
	require.True(
		t,
		strings.HasPrefix(identityURL, "http://") || strings.HasPrefix(identityURL, "https://"),
		"expected absolute identity url, got %q",
		identityURL,
	)
	return identityURL
}

func addIdentityVersionQuery(t *testing.T, rawURL string, version string) string {
	t.Helper()

	parsed, err := url.Parse(rawURL)
	require.NoError(t, err)
	query := parsed.Query()
	query.Set("version", version)
	parsed.RawQuery = query.Encode()
	return parsed.String()
}

func doAnonymousAbsoluteRequest(t *testing.T, targetURL string) *http.Response {
	t.Helper()

	req, err := http.NewRequest(http.MethodGet, targetURL, nil)
	require.NoError(t, err)
	req.Header.Set("Accept", "application/json")

	resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req)
	require.NoError(t, err)
	return resp
}

func assertIdentityURLsForImplicitVersion(t *testing.T, urls map[string]string) {
	t.Helper()

	assertIdentityURLsForExplicitVersion(t, urls)
}

func assertIdentityURLsForExplicitVersion(t *testing.T, urls map[string]string) {
	t.Helper()

	assert.Contains(t, urls["enterprise"], "/services/Soap/c/"+identityExplicitVersion+"/")
	assert.Contains(t, urls["metadata"], "/services/Soap/m/"+identityExplicitVersion+"/")
	assert.Contains(t, urls["partner"], "/services/Soap/u/"+identityExplicitVersion+"/")
	assert.Contains(t, urls["rest"], "/services/data/v"+identityExplicitVersion+"/")
	assert.Contains(t, urls["sobjects"], "/services/data/v"+identityExplicitVersion+"/sobjects/")
	assert.Contains(t, urls["search"], "/services/data/v"+identityExplicitVersion+"/search/")
	assert.Contains(t, urls["query"], "/services/data/v"+identityExplicitVersion+"/query/")
	assert.Contains(t, urls["recent"], "/services/data/v"+identityExplicitVersion+"/recent/")
}

func assertBasicIdentityShape(t *testing.T, body identityResponse) {
	t.Helper()

	assert.NotEmpty(t, body.ID)
	assert.True(t, body.AssertedUser)
	assert.NotEmpty(t, body.UserID)
	assert.NotEmpty(t, body.OrganizationID)
	assert.NotEmpty(t, body.Username)
	assert.NotEmpty(t, body.NickName)
	assert.NotEmpty(t, body.DisplayName)
	assert.NotEmpty(t, body.Email)
	assert.NotNil(t, body.Photos)
	assert.NotEmpty(t, body.Photos["picture"])
	assert.NotEmpty(t, body.Photos["thumbnail"])
	assert.NotNil(t, body.URLs)
	assert.NotEmpty(t, body.URLs["profile"])
	assert.True(t, body.Active)
	assert.NotEmpty(t, body.UserType)
	assert.NotEmpty(t, body.Language)
	assert.NotEmpty(t, body.Locale)
	assert.NotZero(t, body.UTCOffset)
	assert.NotEmpty(t, body.LastModifiedDate)
}
