package integrationservice

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

	"github.com/gin-gonic/gin"
	connectoroauth "github.com/pingcap-inc/nexuscrm-connectors/features/connector/oauth"
	commonv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	mailboxauthorizationv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/mailboxauthorizationv1"
)

const connectedMailHTTPConnectorKey = "connected-mail/release-generation"

type mailboxRuntimeStub struct {
	authorizeBearer     string
	authorizeRequest    mailboxauthorizationv1.Request
	authorizeResponse   mailboxauthorizationv1.Response
	authorizeErr        error
	reauthorizeRequest  mailboxauthorizationv1.ReauthorizeRequest
	reauthorizeResponse mailboxauthorizationv1.Response
	reauthorizeErr      error
	events              *[]string
}

func (stub *mailboxRuntimeStub) AuthorizeConnectedMailbox(
	_ context.Context,
	bearer string,
	request mailboxauthorizationv1.Request,
) (mailboxauthorizationv1.Response, error) {
	stub.authorizeBearer, stub.authorizeRequest = bearer, request
	return stub.authorizeResponse, stub.authorizeErr
}

func (stub *mailboxRuntimeStub) ReauthorizeConnectedMailbox(
	_ context.Context,
	request mailboxauthorizationv1.ReauthorizeRequest,
) (mailboxauthorizationv1.Response, error) {
	stub.reauthorizeRequest = request
	if stub.events != nil {
		*stub.events = append(*stub.events, "reauthorize")
	}
	return stub.reauthorizeResponse, stub.reauthorizeErr
}

type mailboxOAuthStub struct {
	beginResults   []connectoroauth.AuthorizationStart
	beginErr       error
	beginCalls     []connectoroauth.BeginAuthorizationRequest
	completeResult connectoroauth.AuthorizationCompletion
	completeErr    error
	completeCalls  []connectoroauth.CompleteAuthorizationRequest
	principal      *connectoroauth.ConnectedPrincipal
	principalErr   error
	principalUser  string
	principalKey   string
	disconnectErr  error
	disconnectUser string
	disconnectKey  string
	events         *[]string
}

func (stub *mailboxOAuthStub) BeginAuthorization(
	_ context.Context,
	request connectoroauth.BeginAuthorizationRequest,
) (connectoroauth.AuthorizationStart, error) {
	stub.beginCalls = append(stub.beginCalls, request)
	if stub.events != nil {
		*stub.events = append(*stub.events, "begin")
	}
	if stub.beginErr != nil || len(stub.beginResults) == 0 {
		return connectoroauth.AuthorizationStart{}, stub.beginErr
	}
	result := stub.beginResults[0]
	stub.beginResults = stub.beginResults[1:]
	return result, nil
}

func (stub *mailboxOAuthStub) CompleteAuthorization(
	_ context.Context,
	request connectoroauth.CompleteAuthorizationRequest,
) (connectoroauth.AuthorizationCompletion, error) {
	stub.completeCalls = append(stub.completeCalls, request)
	if stub.events != nil {
		*stub.events = append(*stub.events, "complete")
	}
	return stub.completeResult, stub.completeErr
}

func (stub *mailboxOAuthStub) Principal(
	_ context.Context,
	userID string,
	connectorKey string,
) (*connectoroauth.ConnectedPrincipal, error) {
	stub.principalUser, stub.principalKey = userID, connectorKey
	return stub.principal, stub.principalErr
}

func (stub *mailboxOAuthStub) Disconnect(
	_ context.Context,
	userID string,
	connectorKey string,
) error {
	stub.disconnectUser, stub.disconnectKey = userID, connectorKey
	return stub.disconnectErr
}

func TestConnectedMailAuthorizeBindsRuntimeSubjectToProviderCallbackState(t *testing.T) {
	now := time.Now().UTC()
	runtime := &mailboxRuntimeStub{
		authorizeResponse: mailboxRuntimeResponse("user-1", "runtime-handle", 41),
	}
	oauth := &mailboxOAuthStub{beginResults: []connectoroauth.AuthorizationStart{{
		Outcome:   connectoroauth.AuthorizationOutcomeRedirect,
		URL:       "https://auth.example/authorize?access_type=offline&state=internal-oauth-state",
		ExpiresAt: now.Add(time.Minute),
	}}}
	surface, codec := testConnectedMailSurface(t, now, runtime, oauth)
	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(
		http.MethodGet,
		"/api/connected-mailboxes/example/authorize?returnTo=%2FContact%2F003",
		nil,
	)
	request.Header.Set("Authorization", "Bearer delegated-token")
	surface.ServeHTTP(recorder, request)
	if recorder.Code != http.StatusOK || len(recorder.Result().Cookies()) != 0 {
		t.Fatalf(
			"response = %d cookies=%v body=%s",
			recorder.Code,
			recorder.Result().Cookies(),
			recorder.Body.String(),
		)
	}
	var body struct {
		AuthorizationURL string `json:"authorizationUrl"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
		t.Fatalf("body = %s: %v", recorder.Body.String(), err)
	}
	authorizationURL, err := url.Parse(body.AuthorizationURL)
	if err != nil {
		t.Fatal(err)
	}
	wrapped, err := codec.open(authorizationURL.Query().Get("state"))
	if err != nil {
		t.Fatal(err)
	}
	if wrapped.OAuthState != "internal-oauth-state" ||
		wrapped.AuthorizedSubjectID != "user-1" ||
		wrapped.ReauthorizationHandle != "runtime-handle" ||
		wrapped.ConnectorKey != connectedMailHTTPConnectorKey ||
		wrapped.ReturnTarget != "/Contact/003" {
		t.Fatalf("wrapped state = %#v", wrapped)
	}
	if runtime.authorizeBearer != "delegated-token" ||
		runtime.authorizeRequest.Operation != mailboxauthorizationv1.OperationBegin ||
		runtime.authorizeRequest.MinCatalogRevision == nil ||
		*runtime.authorizeRequest.MinCatalogRevision != 41 {
		t.Fatalf(
			"Runtime authorization = bearer %q request %#v",
			runtime.authorizeBearer,
			runtime.authorizeRequest,
		)
	}
	if len(oauth.beginCalls) != 1 ||
		oauth.beginCalls[0].UserID != "user-1" ||
		oauth.beginCalls[0].ConnectorKey != connectedMailHTTPConnectorKey ||
		oauth.beginCalls[0].RedirectURI != "https://crm.example/api/connected-mailboxes/example/callback" {
		t.Fatalf("OAuth begin calls = %#v", oauth.beginCalls)
	}
}

func TestConnectedMailCallbackReadmitsWithRuntimeBeforeStagingProviderResult(t *testing.T) {
	now := time.Now().UTC()
	events := make([]string, 0, 3)
	runtime := &mailboxRuntimeStub{
		reauthorizeResponse: mailboxRuntimeResponse("user-1", "", 41),
		events:              &events,
	}
	oauth := &mailboxOAuthStub{
		completeResult: connectoroauth.AuthorizationCompletion{
			Outcome: connectoroauth.AuthorizationCompletionResultStaged,
		},
		beginResults: []connectoroauth.AuthorizationStart{{
			Outcome: connectoroauth.AuthorizationOutcomeConnected,
			Principal: &connectoroauth.ConnectedPrincipal{
				UserID: "user-1", ConnectorKey: connectedMailHTTPConnectorKey,
				Generation: 1, PrincipalAddress: "rep@example.com",
				State: connectoroauth.PrincipalConnected,
			},
		}},
		events: &events,
	}
	surface, codec := testConnectedMailSurface(t, now, runtime, oauth)
	state := sealConnectedMailTestState(t, codec, now, "user-1", connectedMailHTTPConnectorKey)
	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(
		http.MethodGet,
		"/api/connected-mailboxes/example/callback?state="+url.QueryEscape(state)+"&code=provider-code",
		nil,
	)
	surface.ServeHTTP(recorder, request)
	if recorder.Code != http.StatusFound ||
		recorder.Header().Get("Location") != "https://crm.example/Contact/003" {
		t.Fatalf(
			"response = %d location=%q body=%s",
			recorder.Code,
			recorder.Header().Get("Location"),
			recorder.Body.String(),
		)
	}
	if strings.Join(events, ",") != "reauthorize,complete,begin" {
		t.Fatalf("callback event order = %v", events)
	}
	if len(oauth.completeCalls) != 1 ||
		oauth.completeCalls[0].UserID != "user-1" ||
		oauth.completeCalls[0].State != "internal-oauth-state" ||
		oauth.completeCalls[0].Code != "provider-code" {
		t.Fatalf("OAuth completion = %#v", oauth.completeCalls)
	}
	if runtime.reauthorizeRequest.ReauthorizationHandle != "runtime-handle" ||
		runtime.reauthorizeRequest.MinCatalogRevision == nil ||
		*runtime.reauthorizeRequest.MinCatalogRevision != 41 ||
		runtime.authorizeBearer != "" {
		t.Fatalf("Runtime callback authorization = %#v", runtime.reauthorizeRequest)
	}
	if len(oauth.beginCalls) != 1 || oauth.beginCalls[0].ConnectorKey != connectedMailHTTPConnectorKey {
		t.Fatalf("OAuth resume = %#v", oauth.beginCalls)
	}
}

func TestConnectedMailCallbackDoesNotExchangeProviderCodeWhenRuntimeReadmissionFails(t *testing.T) {
	now := time.Now().UTC()
	runtime := &mailboxRuntimeStub{reauthorizeErr: errors.New("authority changed")}
	oauth := &mailboxOAuthStub{
		completeResult: connectoroauth.AuthorizationCompletion{
			Outcome: connectoroauth.AuthorizationCompletionResultStaged,
		},
		beginResults: []connectoroauth.AuthorizationStart{{
			Outcome: connectoroauth.AuthorizationOutcomeConnected,
		}},
	}
	surface, codec := testConnectedMailSurface(t, now, runtime, oauth)
	state := sealConnectedMailTestState(t, codec, now, "user-1", connectedMailHTTPConnectorKey)
	recorder := httptest.NewRecorder()
	surface.ServeHTTP(
		recorder,
		httptest.NewRequest(
			http.MethodGet,
			"/api/connected-mailboxes/example/callback?state="+
				url.QueryEscape(state)+"&code=provider-code",
			nil,
		),
	)
	if recorder.Code != http.StatusServiceUnavailable {
		t.Fatalf("response = %d %s", recorder.Code, recorder.Body.String())
	}
	if len(oauth.completeCalls) != 0 || len(oauth.beginCalls) != 0 ||
		runtime.reauthorizeRequest.ReauthorizationHandle != "runtime-handle" {
		t.Fatalf(
			"callback calls = complete:%#v reauthorize:%#v begin:%#v",
			oauth.completeCalls,
			runtime.reauthorizeRequest,
			oauth.beginCalls,
		)
	}
}

func TestConnectedMailStatusAndDisconnectUseRuntimeAuthorizedSubject(t *testing.T) {
	now := time.Now().UTC()
	t.Run("connected status", func(t *testing.T) {
		runtime := &mailboxRuntimeStub{
			authorizeResponse: mailboxRuntimeResponse("user-1", "", 41),
		}
		oauth := &mailboxOAuthStub{principal: &connectoroauth.ConnectedPrincipal{
			UserID: "user-1", ConnectorKey: connectedMailHTTPConnectorKey,
			Generation: 1, PrincipalAddress: "rep@example.com",
			State: connectoroauth.PrincipalConnected,
		}}
		surface, _ := testConnectedMailSurface(t, now, runtime, oauth)
		recorder := authenticatedMailboxRequest(surface, http.MethodGet, "/api/connected-mailboxes/example")
		if recorder.Code != http.StatusOK ||
			!strings.Contains(recorder.Body.String(), `"principalAddress":"rep@example.com"`) ||
			runtime.authorizeRequest.Operation != mailboxauthorizationv1.OperationStatus ||
			oauth.principalUser != "user-1" || oauth.principalKey != connectedMailHTTPConnectorKey {
			t.Fatalf("status response = %d %s", recorder.Code, recorder.Body.String())
		}
	})
	t.Run("missing status", func(t *testing.T) {
		runtime := &mailboxRuntimeStub{
			authorizeResponse: mailboxRuntimeResponse("user-1", "", 41),
		}
		oauth := &mailboxOAuthStub{principalErr: connectoroauth.ErrPrincipalNotFound}
		surface, _ := testConnectedMailSurface(t, now, runtime, oauth)
		recorder := authenticatedMailboxRequest(surface, http.MethodGet, "/api/connected-mailboxes/example")
		if recorder.Code != http.StatusOK || strings.TrimSpace(recorder.Body.String()) != "{\"connected\":false}" {
			t.Fatalf("status response = %d %s", recorder.Code, recorder.Body.String())
		}
	})
	t.Run("repository failure", func(t *testing.T) {
		runtime := &mailboxRuntimeStub{
			authorizeResponse: mailboxRuntimeResponse("user-1", "", 41),
		}
		oauth := &mailboxOAuthStub{principalErr: errors.New("repository unavailable")}
		surface, _ := testConnectedMailSurface(t, now, runtime, oauth)
		recorder := authenticatedMailboxRequest(surface, http.MethodGet, "/api/connected-mailboxes/example")
		if recorder.Code != http.StatusServiceUnavailable {
			t.Fatalf("status response = %d %s", recorder.Code, recorder.Body.String())
		}
	})
	t.Run("disconnect after local commit", func(t *testing.T) {
		runtime := &mailboxRuntimeStub{
			authorizeResponse: mailboxRuntimeResponse("user-1", "", 41),
		}
		oauth := &mailboxOAuthStub{disconnectErr: connectoroauth.ErrManualActionRequired}
		surface, _ := testConnectedMailSurface(t, now, runtime, oauth)
		recorder := authenticatedMailboxRequest(surface, http.MethodDelete, "/api/connected-mailboxes/example")
		if recorder.Code != http.StatusNoContent ||
			runtime.authorizeRequest.Operation != mailboxauthorizationv1.OperationDisconnect ||
			oauth.disconnectUser != "user-1" || oauth.disconnectKey != connectedMailHTTPConnectorKey {
			t.Fatalf("disconnect response = %d %s", recorder.Code, recorder.Body.String())
		}
	})
}

func TestConnectedMailHTTPFailsClosedOnForeignAuthorityAndProvider(t *testing.T) {
	now := time.Now().UTC()
	tests := []struct {
		name       string
		runtime    *mailboxRuntimeStub
		oauth      *mailboxOAuthStub
		authorize  bool
		wantStatus int
	}{
		{
			name: "missing browser bearer", runtime: &mailboxRuntimeStub{},
			oauth: &mailboxOAuthStub{}, wantStatus: http.StatusUnauthorized,
		},
		{
			name:    "Runtime denial",
			runtime: &mailboxRuntimeStub{authorizeErr: errors.New("forbidden")},
			oauth:   &mailboxOAuthStub{}, authorize: true, wantStatus: http.StatusForbidden,
		},
		{
			name: "foreign Catalog receipt",
			runtime: &mailboxRuntimeStub{
				authorizeResponse: mailboxRuntimeResponse("user-1", "runtime-handle", 42),
			},
			oauth: &mailboxOAuthStub{}, authorize: true, wantStatus: http.StatusServiceUnavailable,
		},
		{
			name: "foreign provider redirect",
			runtime: &mailboxRuntimeStub{
				authorizeResponse: mailboxRuntimeResponse("user-1", "runtime-handle", 41),
			},
			oauth: &mailboxOAuthStub{beginResults: []connectoroauth.AuthorizationStart{{
				Outcome:   connectoroauth.AuthorizationOutcomeRedirect,
				URL:       "https://evil.example/authorize?state=internal-state",
				ExpiresAt: now.Add(time.Minute),
			}}},
			authorize: true, wantStatus: http.StatusConflict,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			surface, _ := testConnectedMailSurface(t, now, test.runtime, test.oauth)
			recorder := httptest.NewRecorder()
			request := httptest.NewRequest(
				http.MethodGet,
				"/api/connected-mailboxes/example/authorize",
				nil,
			)
			if test.authorize {
				request.Header.Set("Authorization", "Bearer delegated-token")
			}
			surface.ServeHTTP(recorder, request)
			if recorder.Code != test.wantStatus {
				t.Fatalf("response = %d %s", recorder.Code, recorder.Body.String())
			}
		})
	}
}

func TestConnectedMailCallbackRejectsUnboundInputsBeforeEffects(t *testing.T) {
	now := time.Now().UTC()
	baseRuntime := func() *mailboxRuntimeStub {
		return &mailboxRuntimeStub{
			reauthorizeResponse: mailboxRuntimeResponse("user-1", "", 41),
		}
	}
	baseOAuth := func() *mailboxOAuthStub {
		return &mailboxOAuthStub{completeResult: connectoroauth.AuthorizationCompletion{
			Outcome: connectoroauth.AuthorizationCompletionResultStaged,
		}}
	}
	tests := []struct {
		name       string
		requestURL func(*testing.T, *mailboxStateCodec) string
		wantStatus int
	}{
		{
			name: "tampered state",
			requestURL: func(t *testing.T, codec *mailboxStateCodec) string {
				state := sealConnectedMailTestState(t, codec, now, "user-1", connectedMailHTTPConnectorKey)
				return "/api/connected-mailboxes/example/callback?state=" +
					url.QueryEscape(state+"x") + "&code=provider-code"
			},
			wantStatus: http.StatusUnauthorized,
		},
		{
			name: "foreign connector generation",
			requestURL: func(t *testing.T, codec *mailboxStateCodec) string {
				state := sealConnectedMailTestState(t, codec, now, "user-1", "foreign-generation")
				return "/api/connected-mailboxes/example/callback?state=" +
					url.QueryEscape(state) + "&code=provider-code"
			},
			wantStatus: http.StatusUnauthorized,
		},
		{
			name: "duplicate provider code",
			requestURL: func(t *testing.T, codec *mailboxStateCodec) string {
				state := sealConnectedMailTestState(t, codec, now, "user-1", connectedMailHTTPConnectorKey)
				return "/api/connected-mailboxes/example/callback?state=" +
					url.QueryEscape(state) + "&code=first&code=second"
			},
			wantStatus: http.StatusBadRequest,
		},
		{
			name: "provider denial",
			requestURL: func(t *testing.T, codec *mailboxStateCodec) string {
				state := sealConnectedMailTestState(t, codec, now, "user-1", connectedMailHTTPConnectorKey)
				return "/api/connected-mailboxes/example/callback?state=" +
					url.QueryEscape(state) + "&error=access_denied"
			},
			wantStatus: http.StatusBadRequest,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			runtime, oauth := baseRuntime(), baseOAuth()
			surface, codec := testConnectedMailSurface(t, now, runtime, oauth)
			recorder := httptest.NewRecorder()
			surface.ServeHTTP(
				recorder,
				httptest.NewRequest(http.MethodGet, test.requestURL(t, codec), nil),
			)
			if recorder.Code != test.wantStatus {
				t.Fatalf("response = %d %s", recorder.Code, recorder.Body.String())
			}
			if len(oauth.completeCalls) != 0 || runtime.reauthorizeRequest.ReauthorizationHandle != "" {
				t.Fatalf(
					"rejected callback executed effects: complete=%#v reauthorize=%#v",
					oauth.completeCalls,
					runtime.reauthorizeRequest,
				)
			}
		})
	}
}

func testConnectedMailSurface(
	t *testing.T,
	now time.Time,
	runtime *mailboxRuntimeStub,
	oauth *mailboxOAuthStub,
) (http.Handler, *mailboxStateCodec) {
	t.Helper()
	definition, err := parseConnectedMailDefinition([]byte(validConnectedMailDefinitionJSON))
	if err != nil {
		t.Fatal(err)
	}
	config := &ConnectedMailConfig{
		Definition:    definition,
		CallbackURI:   "https://crm.example/api/connected-mailboxes/example/callback",
		ReturnBaseURL: "https://crm.example",
	}
	codec, err := newMailboxStateCodec([]byte(strings.Repeat("k", 32)))
	if err != nil {
		t.Fatal(err)
	}
	codec.now = func() time.Time { return now }
	handler, err := newConnectedMailHandler(
		config,
		connectedMailHTTPConnectorKey,
		41,
		runtime,
		oauth,
		codec,
	)
	if err != nil {
		t.Fatal(err)
	}
	gin.SetMode(gin.TestMode)
	engine := gin.New()
	handler.RegisterRoutes(engine)
	return engine, codec
}

func sealConnectedMailTestState(
	t *testing.T,
	codec *mailboxStateCodec,
	now time.Time,
	subject string,
	connectorKey string,
) string {
	t.Helper()
	state, err := codec.seal(mailboxState{
		OAuthState: "internal-oauth-state", AuthorizedSubjectID: subject,
		ReauthorizationHandle: "runtime-handle", ConnectorKey: connectorKey,
		CallbackURI:  "https://crm.example/api/connected-mailboxes/example/callback",
		ScopeDigest:  mailboxScopeDigest([]string{"mail.send", "openid"}),
		ReturnTarget: "/Contact/003", ExpiresAt: now.Add(time.Minute),
	})
	if err != nil {
		t.Fatal(err)
	}
	return state
}

func authenticatedMailboxRequest(surface http.Handler, method, target string) *httptest.ResponseRecorder {
	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(method, target, nil)
	request.Header.Set("Authorization", "Bearer delegated-token")
	surface.ServeHTTP(recorder, request)
	return recorder
}

func mailboxRuntimeResponse(
	subject string,
	handle string,
	revision uint64,
) mailboxauthorizationv1.Response {
	return mailboxauthorizationv1.Response{
		AuthorizedSubjectID: subject, ReauthorizationHandle: handle,
		Receipts: commonv1.Receipts{
			ProtocolVersion: "1.0", CapabilityVersion: mailboxauthorizationv1.CapabilityName,
			CatalogRevision: revision, SchemaGeneration: 1, EngineGeneration: 1, SecurityEpoch: 1,
		},
	}
}
