package execution

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

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

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

var _ Target = &mockTarget{}

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

func (e *mockTarget) GetTargetID() string {
	return e.TargetID
}
func (e *mockTarget) IsInterruptOnError() bool {
	return e.InterruptOnError
}
func (e *mockTarget) GetEndpoint() string {
	return e.Endpoint
}
func (e *mockTarget) GetTargetType() domain.TargetType {
	return e.TargetType
}
func (e *mockTarget) GetTimeout() time.Duration {
	return e.Timeout
}

func Test_Call(t *testing.T) {
	type args struct {
		ctx        context.Context
		timeout    time.Duration
		sleep      time.Duration
		method     string
		body       []byte
		respBody   []byte
		statusCode int
	}
	type res struct {
		body    []byte
		wantErr bool
	}
	tests := []struct {
		name string
		args args
		res  res
	}{
		{
			"not ok status",
			args{
				ctx:        context.Background(),
				timeout:    time.Minute,
				sleep:      time.Second,
				method:     http.MethodPost,
				body:       []byte("{\"request\": \"values\"}"),
				respBody:   []byte("{\"response\": \"values\"}"),
				statusCode: http.StatusBadRequest,
			},
			res{
				wantErr: true,
			},
		},
		{
			"timeout",
			args{
				ctx:        context.Background(),
				timeout:    time.Second,
				sleep:      time.Second,
				method:     http.MethodPost,
				body:       []byte("{\"request\": \"values\"}"),
				respBody:   []byte("{\"response\": \"values\"}"),
				statusCode: http.StatusOK,
			},
			res{
				wantErr: true,
			},
		},
		{
			"ok",
			args{
				ctx:        context.Background(),
				timeout:    time.Minute,
				sleep:      time.Second,
				method:     http.MethodPost,
				body:       []byte("{\"request\": \"values\"}"),
				respBody:   []byte("{\"response\": \"values\"}"),
				statusCode: http.StatusOK,
			},
			res{
				body: []byte("{\"response\": \"values\"}"),
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			respBody, err := testServerCall(t,
				tt.args.method,
				tt.args.body,
				tt.args.sleep,
				tt.args.statusCode,
				tt.args.respBody,
				testCall(tt.args.ctx, tt.args.timeout, tt.args.body),
			)
			if tt.res.wantErr {
				assert.Error(t, err)
				assert.Nil(t, respBody)
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tt.res.body, respBody)
			}
		})
	}
}

func testCall(ctx context.Context, timeout time.Duration, body []byte) func(string) ([]byte, error) {
	return func(url string) ([]byte, error) {
		return call(ctx, url, timeout, body)
	}
}

func testCallTarget(ctx context.Context,
	target *mockTarget,
	info ContextInfoRequest,
) func(string) ([]byte, error) {
	return func(url string) (r []byte, err error) {
		target.Endpoint = url
		return CallTarget(ctx, target, info)
	}
}

func testServerCall(
	t *testing.T,
	method string,
	body []byte,
	timeout time.Duration,
	statusCode int,
	respBody []byte,
	call func(string) ([]byte, error),
) ([]byte, error) {
	handler := func(w http.ResponseWriter, r *http.Request) {
		checkRequest(t, r, method, body)

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

		time.Sleep(timeout)

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

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

	return call(server.URL)
}

func checkRequest(t *testing.T, sent *http.Request, method string, expectedBody []byte) {
	sentBody, err := io.ReadAll(sent.Body)
	require.NoError(t, err)
	require.Equal(t, expectedBody, sentBody)
	require.Equal(t, method, sent.Method)
}

var _ ContextInfoRequest = &mockContextInfoRequest{}

type request struct {
	Request string `json:"request"`
}

type mockContextInfoRequest struct {
	Request *request `json:"request"`
}

func newMockContextInfoRequest(s string) *mockContextInfoRequest {
	return &mockContextInfoRequest{&request{s}}
}

func (c *mockContextInfoRequest) GetHTTPRequestBody() []byte {
	data, _ := json.Marshal(c)
	return data
}

func (c *mockContextInfoRequest) GetContent() []byte {
	data, _ := json.Marshal(c.Request)
	return data
}

func Test_CallTarget(t *testing.T) {
	type args struct {
		ctx    context.Context
		target *mockTarget
		sleep  time.Duration

		info ContextInfoRequest

		method string
		body   []byte

		respBody   []byte
		statusCode int
	}
	type res struct {
		body    []byte
		wantErr bool
	}
	tests := []struct {
		name string
		args args
		res  res
	}{
		{
			"unknown targettype, error",
			args{
				ctx:    context.Background(),
				sleep:  time.Second,
				method: http.MethodPost,
				info:   newMockContextInfoRequest("content1"),
				target: &mockTarget{
					TargetType: 4,
				},
				body:       []byte("{\"request\":{\"request\":\"content1\"}}"),
				respBody:   []byte("{\"request\":\"content2\"}"),
				statusCode: http.StatusInternalServerError,
			},
			res{
				wantErr: true,
			},
		},
		{
			"webhook, error",
			args{
				ctx:    context.Background(),
				sleep:  time.Second,
				method: http.MethodPost,
				info:   newMockContextInfoRequest("content1"),
				target: &mockTarget{
					TargetType: domain.TargetTypeWebhook,
					Timeout:    time.Minute,
				},
				body:       []byte("{\"request\":{\"request\":\"content1\"}}"),
				respBody:   []byte("{\"request\":\"content2\"}"),
				statusCode: http.StatusInternalServerError,
			},
			res{
				wantErr: true,
			},
		},
		{
			"webhook, ok",
			args{
				ctx:    context.Background(),
				sleep:  time.Second,
				method: http.MethodPost,
				info:   newMockContextInfoRequest("content1"),
				target: &mockTarget{
					TargetType: domain.TargetTypeWebhook,
					Timeout:    time.Minute,
				},
				body:       []byte("{\"request\":{\"request\":\"content1\"}}"),
				respBody:   []byte("{\"request\":\"content2\"}"),
				statusCode: http.StatusOK,
			},
			res{
				body: nil,
			},
		},
		{
			"request response, error",
			args{
				ctx:    context.Background(),
				sleep:  time.Second,
				method: http.MethodPost,
				info:   newMockContextInfoRequest("content1"),
				target: &mockTarget{
					TargetType: domain.TargetTypeCall,
					Timeout:    time.Minute,
				},
				body:       []byte("{\"request\":{\"request\":\"content1\"}}"),
				respBody:   []byte("{\"request\":\"content2\"}"),
				statusCode: http.StatusInternalServerError,
			},
			res{
				wantErr: true,
			},
		},
		{
			"request response, ok",
			args{
				ctx:    context.Background(),
				sleep:  time.Second,
				method: http.MethodPost,
				info:   newMockContextInfoRequest("content1"),
				target: &mockTarget{
					TargetType: domain.TargetTypeCall,
					Timeout:    time.Minute,
				},
				body:       []byte("{\"request\":{\"request\":\"content1\"}}"),
				respBody:   []byte("{\"request\":\"content2\"}"),
				statusCode: http.StatusOK,
			},
			res{
				body: []byte("{\"request\":\"content2\"}"),
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			respBody, err := testServerCall(t,
				tt.args.method,
				tt.args.body,
				tt.args.sleep,
				tt.args.statusCode,
				tt.args.respBody,
				testCallTarget(tt.args.ctx, tt.args.target, tt.args.info),
			)
			if tt.res.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
			assert.Equal(t, tt.res.body, respBody)
		})
	}
}