package middleware

import (
	"context"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"reflect"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"

	"github.com/zitadel/zitadel/internal/domain"
	"github.com/zitadel/zitadel/internal/execution"
)

var _ execution.Target = &mockExecutionTarget{}

type mockExecutionTarget struct {
	InstanceID       string
	ExecutionID      string
	TargetID         string
	TargetType       domain.TargetType
	Endpoint         string
	Timeout          time.Duration
	InterruptOnError bool
}

func (e *mockExecutionTarget) SetEndpoint(endpoint string) {
	e.Endpoint = endpoint
}
func (e *mockExecutionTarget) IsInterruptOnError() bool {
	return e.InterruptOnError
}
func (e *mockExecutionTarget) GetEndpoint() string {
	return e.Endpoint
}
func (e *mockExecutionTarget) GetTargetType() domain.TargetType {
	return e.TargetType
}
func (e *mockExecutionTarget) GetTimeout() time.Duration {
	return e.Timeout
}
func (e *mockExecutionTarget) GetTargetID() string {
	return e.TargetID
}
func (e *mockExecutionTarget) GetExecutionID() string {
	return e.ExecutionID
}

type mockContentRequest struct {
	Content string
}

func newMockContentRequest(content string) *mockContentRequest {
	return &mockContentRequest{
		Content: content,
	}
}

func newMockContextInfoRequest(fullMethod, request string) *ContextInfoRequest {
	return &ContextInfoRequest{
		FullMethod: fullMethod,
		Request:    newMockContentRequest(request),
	}
}

func newMockContextInfoResponse(fullMethod, request, response string) *ContextInfoResponse {
	return &ContextInfoResponse{
		FullMethod: fullMethod,
		Request:    newMockContentRequest(request),
		Response:   newMockContentRequest(response),
	}
}

func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
	type target struct {
		reqBody    execution.ContextInfo
		sleep      time.Duration
		statusCode int
		respBody   interface{}
	}
	type args struct {
		ctx context.Context

		executionTargets []execution.Target
		targets          []target
		fullMethod       string
		req              interface{}
	}
	type res struct {
		want    interface{}
		wantErr bool
	}
	tests := []struct {
		name string
		args args
		res  res
	}{
		{
			"target, executionTargets nil",
			args{
				ctx:              context.Background(),
				fullMethod:       "/service/method",
				executionTargets: nil,
				req:              newMockContentRequest("request"),
			},
			res{
				want: newMockContentRequest("request"),
			},
		},
		{
			"target, executionTargets empty",
			args{
				ctx:              context.Background(),
				fullMethod:       "/service/method",
				executionTargets: []execution.Target{},
				req:              newMockContentRequest("request"),
			},
			res{
				want: newMockContentRequest("request"),
			},
		},
		{
			"target, not reachable",
			args{
				ctx:        context.Background(),
				fullMethod: "/service/method",
				executionTargets: []execution.Target{
					&mockExecutionTarget{
						InstanceID:       "instance",
						ExecutionID:      "request./zitadel.session.v2.SessionService/SetSession",
						TargetID:         "target",
						TargetType:       domain.TargetTypeCall,
						Timeout:          time.Minute,
						InterruptOnError: true,
					},
				},
				targets: []target{},
				req:     newMockContentRequest("content"),
			},
			res{
				wantErr: true,
			},
		},
		{
			"target, error without interrupt",
			args{
				ctx:        context.Background(),
				fullMethod: "/service/method",
				executionTargets: []execution.Target{
					&mockExecutionTarget{
						InstanceID:  "instance",
						ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
						TargetID:    "target",
						TargetType:  domain.TargetTypeCall,
						Timeout:     time.Minute,
					},
				},
				targets: []target{
					{
						reqBody:    newMockContextInfoRequest("/service/method", "content"),
						respBody:   newMockContentRequest("content1"),
						sleep:      0,
						statusCode: http.StatusBadRequest,
					},
				},
				req: newMockContentRequest("content"),
			},
			res{
				want: newMockContentRequest("content"),
			},
		},
		{
			"target, interruptOnError",
			args{
				ctx:        context.Background(),
				fullMethod: "/service/method",
				executionTargets: []execution.Target{
					&mockExecutionTarget{
						InstanceID:       "instance",
						ExecutionID:      "request./zitadel.session.v2.SessionService/SetSession",
						TargetID:         "target",
						TargetType:       domain.TargetTypeCall,
						Timeout:          time.Minute,
						InterruptOnError: true,
					},
				},

				targets: []target{
					{
						reqBody:    newMockContextInfoRequest("/service/method", "content"),
						respBody:   newMockContentRequest("content1"),
						sleep:      0,
						statusCode: http.StatusBadRequest,
					},
				},
				req: newMockContentRequest("content"),
			},
			res{
				wantErr: true,
			},
		},
		{
			"target, timeout",
			args{
				ctx:        context.Background(),
				fullMethod: "/service/method",
				executionTargets: []execution.Target{
					&mockExecutionTarget{
						InstanceID:       "instance",
						ExecutionID:      "request./zitadel.session.v2.SessionService/SetSession",
						TargetID:         "target",
						TargetType:       domain.TargetTypeCall,
						Timeout:          time.Second,
						InterruptOnError: true,
					},
				},
				targets: []target{
					{
						reqBody:    newMockContextInfoRequest("/service/method", "content"),
						respBody:   newMockContentRequest("content1"),
						sleep:      5 * time.Second,
						statusCode: http.StatusOK,
					},
				},
				req: newMockContentRequest("content"),
			},
			res{
				wantErr: true,
			},
		},
		{
			"target, wrong request",
			args{
				ctx:        context.Background(),
				fullMethod: "/service/method",
				executionTargets: []execution.Target{
					&mockExecutionTarget{
						InstanceID:       "instance",
						ExecutionID:      "request./zitadel.session.v2.SessionService/SetSession",
						TargetID:         "target",
						TargetType:       domain.TargetTypeCall,
						Timeout:          time.Second,
						InterruptOnError: true,
					},
				},
				targets: []target{
					{reqBody: newMockContextInfoRequest("/service/method", "wrong")},
				},
				req: newMockContentRequest("content"),
			},
			res{
				wantErr: true,
			},
		},
		{
			"target, ok",
			args{
				ctx:        context.Background(),
				fullMethod: "/service/method",
				executionTargets: []execution.Target{
					&mockExecutionTarget{
						InstanceID:       "instance",
						ExecutionID:      "request./zitadel.session.v2.SessionService/SetSession",
						TargetID:         "target",
						TargetType:       domain.TargetTypeCall,
						Timeout:          time.Minute,
						InterruptOnError: true,
					},
				},
				targets: []target{
					{
						reqBody:    newMockContextInfoRequest("/service/method", "content"),
						respBody:   newMockContentRequest("content1"),
						sleep:      0,
						statusCode: http.StatusOK,
					},
				},
				req: newMockContentRequest("content"),
			},
			res{
				want: newMockContentRequest("content1"),
			},
		},
		{
			"target async, timeout",
			args{
				ctx:        context.Background(),
				fullMethod: "/service/method",
				executionTargets: []execution.Target{
					&mockExecutionTarget{
						InstanceID:  "instance",
						ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
						TargetID:    "target",
						TargetType:  domain.TargetTypeAsync,
						Timeout:     time.Second,
					},
				},
				targets: []target{
					{
						reqBody:    newMockContextInfoRequest("/service/method", "content"),
						respBody:   newMockContentRequest("content1"),
						sleep:      5 * time.Second,
						statusCode: http.StatusOK,
					},
				},
				req: newMockContentRequest("content"),
			},
			res{
				want: newMockContentRequest("content"),
			},
		},
		{
			"target async, ok",
			args{
				ctx:        context.Background(),
				fullMethod: "/service/method",
				executionTargets: []execution.Target{
					&mockExecutionTarget{
						InstanceID:  "instance",
						ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
						TargetID:    "target",
						TargetType:  domain.TargetTypeAsync,
						Timeout:     time.Minute,
					},
				},
				targets: []target{
					{
						reqBody:    newMockContextInfoRequest("/service/method", "content"),
						respBody:   newMockContentRequest("content1"),
						sleep:      0,
						statusCode: http.StatusOK,
					},
				},
				req: newMockContentRequest("content"),
			},
			res{
				want: newMockContentRequest("content"),
			},
		},
		{
			"webhook, error",
			args{
				ctx:        context.Background(),
				fullMethod: "/service/method",
				executionTargets: []execution.Target{
					&mockExecutionTarget{
						InstanceID:       "instance",
						ExecutionID:      "request./zitadel.session.v2.SessionService/SetSession",
						TargetID:         "target",
						TargetType:       domain.TargetTypeWebhook,
						Timeout:          time.Minute,
						InterruptOnError: true,
					},
				},
				targets: []target{
					{
						reqBody:    newMockContextInfoRequest("/service/method", "content"),
						sleep:      0,
						statusCode: http.StatusInternalServerError,
					},
				},
				req: newMockContentRequest("content"),
			},
			res{
				wantErr: true,
			},
		},
		{
			"webhook, timeout",
			args{
				ctx:        context.Background(),
				fullMethod: "/service/method",
				executionTargets: []execution.Target{
					&mockExecutionTarget{
						InstanceID:       "instance",
						ExecutionID:      "request./zitadel.session.v2.SessionService/SetSession",
						TargetID:         "target",
						TargetType:       domain.TargetTypeWebhook,
						Timeout:          time.Second,
						InterruptOnError: true,
					},
				},
				targets: []target{
					{
						reqBody:    newMockContextInfoRequest("/service/method", "content"),
						respBody:   newMockContentRequest("content1"),
						sleep:      5 * time.Second,
						statusCode: http.StatusOK,
					},
				},
				req: newMockContentRequest("content"),
			},
			res{
				wantErr: true,
			},
		},
		{
			"webhook, ok",
			args{
				ctx:        context.Background(),
				fullMethod: "/service/method",
				executionTargets: []execution.Target{
					&mockExecutionTarget{
						InstanceID:       "instance",
						ExecutionID:      "request./zitadel.session.v2.SessionService/SetSession",
						TargetID:         "target",
						TargetType:       domain.TargetTypeWebhook,
						Timeout:          time.Minute,
						InterruptOnError: true,
					},
				},
				targets: []target{
					{
						reqBody:    newMockContextInfoRequest("/service/method", "content"),
						respBody:   newMockContentRequest("content1"),
						sleep:      0,
						statusCode: http.StatusOK,
					},
				},
				req: newMockContentRequest("content"),
			},
			res{
				want: newMockContentRequest("content"),
			},
		},
		{
			"with includes, interruptOnError",
			args{
				ctx:        context.Background(),
				fullMethod: "/service/method",
				executionTargets: []execution.Target{
					&mockExecutionTarget{
						InstanceID:       "instance",
						ExecutionID:      "request./zitadel.session.v2.SessionService/SetSession",
						TargetID:         "target1",
						TargetType:       domain.TargetTypeCall,
						Timeout:          time.Minute,
						InterruptOnError: true,
					},
					&mockExecutionTarget{
						InstanceID:       "instance",
						ExecutionID:      "request./zitadel.session.v2.SessionService/SetSession",
						TargetID:         "target2",
						TargetType:       domain.TargetTypeCall,
						Timeout:          time.Minute,
						InterruptOnError: true,
					},
					&mockExecutionTarget{
						InstanceID:       "instance",
						ExecutionID:      "request./zitadel.session.v2.SessionService/SetSession",
						TargetID:         "target3",
						TargetType:       domain.TargetTypeCall,
						Timeout:          time.Minute,
						InterruptOnError: true,
					},
				},

				targets: []target{
					{
						reqBody:    newMockContextInfoRequest("/service/method", "content"),
						respBody:   newMockContentRequest("content1"),
						sleep:      0,
						statusCode: http.StatusOK,
					},
					{
						reqBody:    newMockContextInfoRequest("/service/method", "content1"),
						respBody:   newMockContentRequest("content2"),
						sleep:      0,
						statusCode: http.StatusBadRequest,
					},
					{
						reqBody:    newMockContextInfoRequest("/service/method", "content2"),
						respBody:   newMockContentRequest("content3"),
						sleep:      0,
						statusCode: http.StatusOK,
					},
				},
				req: newMockContentRequest("content"),
			},
			res{
				wantErr: true,
			},
		},
		{
			"with includes, timeout",
			args{
				ctx:        context.Background(),
				fullMethod: "/service/method",
				executionTargets: []execution.Target{
					&mockExecutionTarget{
						InstanceID:       "instance",
						ExecutionID:      "request./zitadel.session.v2.SessionService/SetSession",
						TargetID:         "target1",
						TargetType:       domain.TargetTypeCall,
						Timeout:          time.Minute,
						InterruptOnError: true,
					},
					&mockExecutionTarget{
						InstanceID:       "instance",
						ExecutionID:      "request./zitadel.session.v2.SessionService/SetSession",
						TargetID:         "target2",
						TargetType:       domain.TargetTypeCall,
						Timeout:          time.Second,
						InterruptOnError: true,
					},
					&mockExecutionTarget{
						InstanceID:       "instance",
						ExecutionID:      "request./zitadel.session.v2.SessionService/SetSession",
						TargetID:         "target3",
						TargetType:       domain.TargetTypeCall,
						Timeout:          time.Second,
						InterruptOnError: true,
					},
				},
				targets: []target{
					{
						reqBody:    newMockContextInfoRequest("/service/method", "content"),
						respBody:   newMockContentRequest("content1"),
						sleep:      0,
						statusCode: http.StatusOK,
					},
					{
						reqBody:    newMockContextInfoRequest("/service/method", "content1"),
						respBody:   newMockContentRequest("content2"),
						sleep:      5 * time.Second,
						statusCode: http.StatusBadRequest,
					},
					{
						reqBody:    newMockContextInfoRequest("/service/method", "content2"),
						respBody:   newMockContentRequest("content3"),
						sleep:      5 * time.Second,
						statusCode: http.StatusOK,
					},
				},
				req: newMockContentRequest("content"),
			},
			res{
				wantErr: true,
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			closeFuncs := make([]func(), len(tt.args.targets))
			for i, target := range tt.args.targets {
				url, closeF := testServerCall(
					target.reqBody,
					target.sleep,
					target.statusCode,
					target.respBody,
				)

				et := tt.args.executionTargets[i].(*mockExecutionTarget)
				et.SetEndpoint(url)
				closeFuncs[i] = closeF
			}

			resp, err := executeTargetsForRequest(
				tt.args.ctx,
				tt.args.executionTargets,
				tt.args.fullMethod,
				tt.args.req,
			)

			if tt.res.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
			assert.Equal(t, tt.res.want, resp)

			for _, closeF := range closeFuncs {
				closeF()
			}
		})
	}
}

func testServerCall(
	reqBody interface{},
	sleep time.Duration,
	statusCode int,
	respBody interface{},
) (string, func()) {
	handler := func(w http.ResponseWriter, r *http.Request) {
		data, err := json.Marshal(reqBody)
		if err != nil {
			http.Error(w, "error", http.StatusInternalServerError)
			return
		}

		sentBody, err := io.ReadAll(r.Body)
		if err != nil {
			http.Error(w, "error", http.StatusInternalServerError)
			return
		}

		if !reflect.DeepEqual(data, sentBody) {
			http.Error(w, "error", http.StatusInternalServerError)
			return
		}

		if statusCode != http.StatusOK {
			http.Error(w, "error", statusCode)
			return
		}

		time.Sleep(sleep)

		w.Header().Set("Content-Type", "application/json")
		resp, err := json.Marshal(respBody)
		if err != nil {
			http.Error(w, "error", http.StatusInternalServerError)
			return
		}
		if _, err := io.WriteString(w, string(resp)); err != nil {
			http.Error(w, "error", http.StatusInternalServerError)
			return
		}
	}

	server := httptest.NewServer(http.HandlerFunc(handler))

	return server.URL, server.Close
}

func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
	type target struct {
		reqBody    execution.ContextInfo
		sleep      time.Duration
		statusCode int
		respBody   interface{}
	}
	type args struct {
		ctx context.Context

		executionTargets []execution.Target
		targets          []target
		fullMethod       string
		req              interface{}
		resp             interface{}
	}
	type res struct {
		want    interface{}
		wantErr bool
	}
	tests := []struct {
		name string
		args args
		res  res
	}{
		{
			"target, executionTargets nil",
			args{
				ctx:              context.Background(),
				fullMethod:       "/service/method",
				executionTargets: nil,
				req:              newMockContentRequest("request"),
				resp:             newMockContentRequest("response"),
			},
			res{
				want: newMockContentRequest("response"),
			},
		},
		{
			"target, executionTargets empty",
			args{
				ctx:              context.Background(),
				fullMethod:       "/service/method",
				executionTargets: []execution.Target{},
				req:              newMockContentRequest("request"),
				resp:             newMockContentRequest("response"),
			},
			res{
				want: newMockContentRequest("response"),
			},
		},
		{
			"target, empty response",
			args{
				ctx:        context.Background(),
				fullMethod: "/service/method",
				executionTargets: []execution.Target{
					&mockExecutionTarget{
						InstanceID:       "instance",
						ExecutionID:      "request./zitadel.session.v2.SessionService/SetSession",
						TargetID:         "target",
						TargetType:       domain.TargetTypeCall,
						Timeout:          time.Minute,
						InterruptOnError: true,
					},
				},
				targets: []target{
					{
						reqBody:    newMockContextInfoRequest("/service/method", "content"),
						respBody:   newMockContentRequest(""),
						sleep:      0,
						statusCode: http.StatusOK,
					},
				},
				req: []byte{},
			},
			res{
				wantErr: true,
			},
		},
		{
			"target, ok",
			args{
				ctx:        context.Background(),
				fullMethod: "/service/method",
				executionTargets: []execution.Target{
					&mockExecutionTarget{
						InstanceID:       "instance",
						ExecutionID:      "response./zitadel.session.v2.SessionService/SetSession",
						TargetID:         "target",
						TargetType:       domain.TargetTypeCall,
						Timeout:          time.Minute,
						InterruptOnError: true,
					},
				},
				targets: []target{
					{
						reqBody:    newMockContextInfoResponse("/service/method", "request", "response"),
						respBody:   newMockContentRequest("response1"),
						sleep:      0,
						statusCode: http.StatusOK,
					},
				},
				req:  newMockContentRequest("request"),
				resp: newMockContentRequest("response"),
			},
			res{
				want: newMockContentRequest("response1"),
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			closeFuncs := make([]func(), len(tt.args.targets))
			for i, target := range tt.args.targets {
				url, closeF := testServerCall(
					target.reqBody,
					target.sleep,
					target.statusCode,
					target.respBody,
				)

				et := tt.args.executionTargets[i].(*mockExecutionTarget)
				et.SetEndpoint(url)
				closeFuncs[i] = closeF
			}

			resp, err := executeTargetsForResponse(
				tt.args.ctx,
				tt.args.executionTargets,
				tt.args.fullMethod,
				tt.args.req,
				tt.args.resp,
			)

			if tt.res.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
			assert.Equal(t, tt.res.want, resp)

			for _, closeF := range closeFuncs {
				closeF()
			}
		})
	}
}