package connectedmailhttp

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	connector_oauth "github.com/pingcap-inc/nexuscrm-connectors/features/connector/oauth"
)

const (
	testChallenge = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
	testVerifier  = "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv"
)

func TestOAuthAdapterExecutesConfiguredProviderGrammar(t *testing.T) {
	var calls atomic.Int64
	server := httptest.NewTLSServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
		calls.Add(1)
		switch request.URL.Path {
		case "/token":
			if err := request.ParseForm(); err != nil {
				t.Fatal(err)
			}
			if request.Form.Get("client_id") != "client-1" || request.Form.Get("client_secret") != "secret-1" {
				t.Fatalf("token client fields = %#v", request.Form)
			}
			switch request.Form.Get("grant_type") {
			case "authorization_code":
				if request.Form.Get("code") != "code-1" || request.Form.Get("code_verifier") != testVerifier {
					t.Fatalf("exchange form = %#v", request.Form)
				}
				writeJSON(t, response, map[string]any{
					"access_token": "access-1", "refresh_token": "refresh-1", "token_type": "Bearer",
					"scope": "mail.send identity", "expires_in": 3600,
				})
			case "refresh_token":
				if request.Form.Get("refresh_token") != "refresh-1" {
					t.Fatalf("refresh form = %#v", request.Form)
				}
				writeJSON(t, response, map[string]any{"access_token": "access-2", "token_type": "bearer"})
			default:
				t.Fatalf("grant type = %q", request.Form.Get("grant_type"))
			}
		case "/identity":
			if request.Header.Get("Authorization") != "Bearer access-1" {
				t.Fatalf("identity authorization = %q", request.Header.Get("Authorization"))
			}
			writeJSON(t, response, map[string]any{
				"subject": "remote-user-1", "address": "sender@example.com", "display": "Sender",
			})
		case "/revoke":
			if err := request.ParseForm(); err != nil {
				t.Fatal(err)
			}
			if request.Form.Get("token") != "refresh-1" {
				t.Fatalf("revoke form = %#v", request.Form)
			}
			response.WriteHeader(http.StatusNoContent)
		default:
			http.NotFound(response, request)
		}
	}))
	defer server.Close()

	config := testOAuthConfig(server.URL)
	provider, err := NewOAuthAdapter(config, server.Client().Transport, allowAllURLs{})
	if err != nil {
		t.Fatal(err)
	}
	config.AuthorizationParameters["prompt"] = "none"
	authorizationURL, err := provider.AuthorizationURL(connector_oauth.AuthorizationURLRequest{
		State: "state-1", CodeChallenge: testChallenge, ChallengeMethod: "S256",
		RedirectURI: "https://crm.example/callback", Scopes: []string{"mail.send", "identity"},
	})
	if err != nil {
		t.Fatal(err)
	}
	parsed, err := url.Parse(authorizationURL)
	if err != nil {
		t.Fatal(err)
	}
	query := parsed.Query()
	if parsed.Path != "/authorize" || query.Get("client_id") != "client-1" || query.Get("state") != "state-1" ||
		query.Get("code_challenge") != testChallenge || query.Get("access_type") != "offline" || query.Get("prompt") != "consent" ||
		strings.Contains(authorizationURL, "secret-1") {
		t.Fatalf("authorization URL = %q", authorizationURL)
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	tokens, err := provider.Exchange(ctx, connector_oauth.TokenExchangeRequest{
		Code: "code-1", CodeVerifier: testVerifier, RedirectURI: "https://crm.example/callback",
	})
	if err != nil || tokens.AccessToken != "access-1" || tokens.RefreshToken != "refresh-1" ||
		strings.Join(tokens.Scopes, " ") != "identity mail.send" || tokens.ExpiresAt == nil {
		t.Fatalf("Exchange = %#v, %v", tokens, err)
	}
	identity, err := provider.Identity(ctx, tokens.AccessToken)
	if err != nil || identity.Subject != "remote-user-1" || identity.PrincipalAddress != "sender@example.com" ||
		identity.Issuer != "https://issuer.example" {
		t.Fatalf("Identity = %#v, %v", identity, err)
	}
	refreshed, err := provider.Refresh(ctx, tokens.RefreshToken)
	if err != nil || refreshed.AccessToken != "access-2" || refreshed.Scopes != nil {
		t.Fatalf("Refresh = %#v, %v", refreshed, err)
	}
	if err := provider.Revoke(ctx, tokens.RefreshToken); err != nil {
		t.Fatal(err)
	}
	if calls.Load() != 4 || provider.EffectTerminationBound() != time.Second {
		t.Fatalf("provider calls=%d termination=%s", calls.Load(), provider.EffectTerminationBound())
	}
}

func TestOAuthAdapterPreservesOmittedAndExplicitEmptyScopes(t *testing.T) {
	tests := []struct {
		name string
		body string
		want []string
	}{
		{name: "omitted", body: `{"access_token":"access","token_type":"Bearer"}`, want: nil},
		{name: "explicit empty", body: `{"access_token":"access","token_type":"Bearer","scope":""}`, want: []string{}},
		{name: "explicit grant", body: `{"access_token":"access","token_type":"Bearer","scope":"mail.send identity"}`, want: []string{"identity", "mail.send"}},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			provider, err := NewOAuthAdapter(testOAuthConfig("https://provider.example"), roundTripFunc(func(*http.Request) (*http.Response, error) {
				return jsonResponse(http.StatusOK, test.body), nil
			}), allowAllURLs{})
			if err != nil {
				t.Fatal(err)
			}
			tokens, err := provider.Refresh(context.Background(), "refresh-secret")
			if err != nil || !sameStringSlice(tokens.Scopes, test.want) || (tokens.Scopes == nil) != (test.want == nil) {
				t.Fatalf("Refresh scopes = %#v, %v; want %#v", tokens.Scopes, err, test.want)
			}
		})
	}
}

func TestOAuthAdapterRejectsUnsafeConfiguration(t *testing.T) {
	tests := map[string]func(*OAuthConfig){
		"endpoint query":    func(config *OAuthConfig) { config.TokenEndpoint += "?client_secret=forbidden" },
		"credentialed HTTP": func(config *OAuthConfig) { config.TokenEndpoint = "http://127.0.0.1/token" },
		"reserved parameter": func(config *OAuthConfig) {
			config.AuthorizationParameters = map[string]string{"client_id": "forbidden"}
		},
		"hyphenated API key": func(config *OAuthConfig) { config.AuthorizationParameters = map[string]string{"api-key": "forbidden"} },
		"prefixed API key": func(config *OAuthConfig) {
			config.AuthorizationParameters = map[string]string{"x-api-key": "forbidden"}
		},
		"client assertion": func(config *OAuthConfig) {
			config.AuthorizationParameters = map[string]string{"client-assertion": "forbidden"}
		},
		"unsafe prompt value": func(config *OAuthConfig) {
			config.AuthorizationParameters = map[string]string{"prompt": "secret-value"}
		},
		"excessive bound": func(config *OAuthConfig) { config.TerminationBound = time.Minute + time.Nanosecond },
	}
	for name, mutate := range tests {
		t.Run(name, func(t *testing.T) {
			config := testOAuthConfig("https://provider.example")
			mutate(&config)
			if _, err := NewOAuthAdapter(config, http.DefaultTransport, allowAllURLs{}); !errors.Is(err, connector_oauth.ErrInvalidInput) {
				t.Fatalf("NewOAuthAdapter error = %v", err)
			}
		})
	}
	if _, err := NewOAuthAdapter(testOAuthConfig("https://provider.example"), http.DefaultTransport,
		&mutableAllowlist{err: errors.New("host denied")}); !errors.Is(err, connector_oauth.ErrInvalidInput) {
		t.Fatalf("allowlist error = %v", err)
	}
}

func TestOAuthAdapterDoesNotFollowCredentialedRedirects(t *testing.T) {
	var targetCalls atomic.Int64
	target := httptest.NewTLSServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
		targetCalls.Add(1)
	}))
	defer target.Close()
	var sourceCalls atomic.Int64
	source := httptest.NewTLSServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
		sourceCalls.Add(1)
		response.Header().Set("Location", target.URL+"/capture")
		response.WriteHeader(http.StatusTemporaryRedirect)
	}))
	defer source.Close()

	provider, err := NewOAuthAdapter(testOAuthConfig(source.URL), source.Client().Transport, allowAllURLs{})
	if err != nil {
		t.Fatal(err)
	}
	_, err = provider.Exchange(context.Background(), connector_oauth.TokenExchangeRequest{
		Code: "code-secret", CodeVerifier: testVerifier, RedirectURI: "https://crm.example/callback",
	})
	assertProviderError(t, err, connector_oauth.ProviderErrorUncertain, "temporarily_unavailable")
	if sourceCalls.Load() != 1 || targetCalls.Load() != 0 {
		t.Fatalf("redirect calls source=%d target=%d", sourceCalls.Load(), targetCalls.Load())
	}
}

func TestOAuthAdapterClassifiesPreflightDenialWithoutCallingTransport(t *testing.T) {
	allowlist := &mutableAllowlist{}
	var transportCalls atomic.Int64
	provider, err := NewOAuthAdapter(testOAuthConfig("https://provider.example"), roundTripFunc(func(*http.Request) (*http.Response, error) {
		transportCalls.Add(1)
		return nil, errors.New("must not run")
	}), allowlist)
	if err != nil {
		t.Fatal(err)
	}
	allowlist.set(errors.New("destination denied with secret material"))
	_, err = provider.Refresh(context.Background(), "refresh-secret")
	assertProviderError(t, err, connector_oauth.ProviderErrorRejected, "provider_rejected")
	if transportCalls.Load() != 0 || strings.Contains(err.Error(), "secret") || strings.Contains(err.Error(), "destination") {
		t.Fatalf("preflight result calls=%d error=%q", transportCalls.Load(), err)
	}
}

func TestOAuthAdapterKeepsFormUntilTransportClosesRequestBody(t *testing.T) {
	releaseRead := make(chan struct{})
	readForm := make(chan string, 1)
	provider, err := NewOAuthAdapter(testOAuthConfig("https://provider.example"), roundTripFunc(func(request *http.Request) (*http.Response, error) {
		go func() {
			<-releaseRead
			payload, readErr := io.ReadAll(request.Body)
			if readErr != nil {
				readForm <- "read failed"
			} else {
				readForm <- string(payload)
			}
			_ = request.Body.Close()
		}()
		return jsonResponse(http.StatusOK, `{"access_token":"access","token_type":"Bearer"}`), nil
	}), allowAllURLs{})
	if err != nil {
		t.Fatal(err)
	}
	if _, err := provider.Refresh(context.Background(), "refresh-secret"); err != nil {
		t.Fatal(err)
	}
	close(releaseRead)
	select {
	case payload := <-readForm:
		values, err := url.ParseQuery(payload)
		if err != nil || values.Get("refresh_token") != "refresh-secret" || values.Get("client_secret") != "secret-1" {
			t.Fatalf("asynchronous request form = %q, %v", payload, err)
		}
	case <-time.After(time.Second):
		t.Fatal("transport did not finish reading the request body")
	}
}

func TestOAuthAdapterReturnsOnlySanitizedProviderErrors(t *testing.T) {
	tests := []struct {
		name string
		run  func(*OAuthAdapter) error
		kind connector_oauth.ProviderErrorKind
		code string
	}{
		{name: "exchange transport", run: func(provider *OAuthAdapter) error {
			_, err := provider.Exchange(context.Background(), connector_oauth.TokenExchangeRequest{
				Code: "code-secret", CodeVerifier: testVerifier, RedirectURI: "https://crm.example/callback",
			})
			return err
		}, kind: connector_oauth.ProviderErrorUncertain, code: "transport_error"},
		{name: "identity transport", run: func(provider *OAuthAdapter) error {
			_, err := provider.Identity(context.Background(), "access-secret")
			return err
		}, kind: connector_oauth.ProviderErrorTransient, code: "transport_error"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			provider, err := NewOAuthAdapter(testOAuthConfig("https://provider.example"), roundTripFunc(func(*http.Request) (*http.Response, error) {
				return nil, errors.New("transport failed after write with client-secret")
			}), allowAllURLs{})
			if err != nil {
				t.Fatal(err)
			}
			err = test.run(provider)
			assertProviderError(t, err, test.kind, test.code)
			if strings.Contains(err.Error(), "secret") || strings.Contains(err.Error(), "after write") {
				t.Fatalf("provider error leaked cause: %q", err)
			}
		})
	}
}

func TestOAuthAdapterRejectsMalformedSuccessAsUncertain(t *testing.T) {
	for name, body := range map[string]string{
		"trailing data": `{"access_token":"access","token_type":"Bearer"}{}`,
		"null scope":    `{"access_token":"access","token_type":"Bearer","scope":null}`,
		"wrong type":    `{"access_token":"access","token_type":"MAC"}`,
	} {
		t.Run(name, func(t *testing.T) {
			provider, err := NewOAuthAdapter(testOAuthConfig("https://provider.example"), roundTripFunc(func(*http.Request) (*http.Response, error) {
				return jsonResponse(http.StatusOK, body), nil
			}), allowAllURLs{})
			if err != nil {
				t.Fatal(err)
			}
			_, err = provider.Refresh(context.Background(), "refresh-secret")
			assertProviderError(t, err, connector_oauth.ProviderErrorUncertain, "provider_error")
		})
	}
}

func testOAuthConfig(base string) OAuthConfig {
	return OAuthConfig{
		AuthorizationEndpoint: base + "/authorize",
		TokenEndpoint:         base + "/token",
		IdentityEndpoint:      base + "/identity",
		RevocationEndpoint:    base + "/revoke",
		Issuer:                "https://issuer.example",
		ClientID:              "client-1",
		ClientSecret:          "secret-1",
		AuthorizationParameters: map[string]string{
			"access_type": "offline",
			"prompt":      "consent",
		},
		IdentitySubjectField:     "subject",
		IdentityAddressField:     "address",
		IdentityDisplayNameField: "display",
		TerminationBound:         time.Second,
	}
}

func writeJSON(t *testing.T, response http.ResponseWriter, value any) {
	t.Helper()
	response.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(response).Encode(value); err != nil {
		t.Fatal(err)
	}
}

func jsonResponse(status int, body string) *http.Response {
	return &http.Response{
		StatusCode: status,
		Header:     http.Header{"Content-Type": []string{"application/json"}},
		Body:       io.NopCloser(strings.NewReader(body)),
	}
}

func assertProviderError(t *testing.T, err error, kind connector_oauth.ProviderErrorKind, code string) {
	t.Helper()
	var providerErr *connector_oauth.ProviderError
	if !errors.As(err, &providerErr) || providerErr.Kind != kind || providerErr.Code != code {
		t.Fatalf("provider error = %#v; want kind=%q code=%q", err, kind, code)
	}
}

func sameStringSlice(left, right []string) bool {
	if len(left) != len(right) {
		return false
	}
	for index := range left {
		if left[index] != right[index] {
			return false
		}
	}
	return true
}

type roundTripFunc func(*http.Request) (*http.Response, error)

func (function roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
	return function(request)
}

type allowAllURLs struct{}

func (allowAllURLs) EnsureAllowed(string) error { return nil }

type mutableAllowlist struct {
	mu  sync.Mutex
	err error
}

func (allowlist *mutableAllowlist) EnsureAllowed(string) error {
	allowlist.mu.Lock()
	defer allowlist.mu.Unlock()
	return allowlist.err
}

func (allowlist *mutableAllowlist) set(err error) {
	allowlist.mu.Lock()
	defer allowlist.mu.Unlock()
	allowlist.err = err
}
