package twofactorverify

import (
	"bytes"
	"context"
	"encoding/json"
	"io"
	"net/http"
	"testing"

	"github.com/stretchr/testify/require"

	"gitlab.com/gitlab-org/gitlab-shell/v14/client/testserver"
	"gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/commandargs"
	"gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/readwriter"
	"gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
	"gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet/twofactorverify"
)

type blockingReader struct{}

func (*blockingReader) Read([]byte) (int, error) {
	waitInfinitely := make(chan struct{})
	<-waitInfinitely

	return 0, nil
}

func setup(t *testing.T) []testserver.TestRequestHandler {
	waitInfinitely := make(chan struct{})
	requests := []testserver.TestRequestHandler{
		{
			Path: "/api/v4/internal/two_factor_manual_otp_check",
			Handler: func(w http.ResponseWriter, r *http.Request) {
				b, err := io.ReadAll(r.Body)
				defer r.Body.Close()

				require.NoError(t, err)

				var requestBody *twofactorverify.RequestBody
				require.NoError(t, json.Unmarshal(b, &requestBody))

				switch requestBody.KeyId {
				case "verify_via_otp", "verify_via_otp_with_push_error":
					body := map[string]interface{}{
						"success": true,
					}
					json.NewEncoder(w).Encode(body)
				case "wait_infinitely":
					<-waitInfinitely
				case "error":
					body := map[string]interface{}{
						"success": false,
						"message": "error message",
					}
					require.NoError(t, json.NewEncoder(w).Encode(body))
				case "broken":
					w.WriteHeader(http.StatusInternalServerError)
				}
			},
		},
		{
			Path: "/api/v4/internal/two_factor_push_otp_check",
			Handler: func(w http.ResponseWriter, r *http.Request) {
				b, err := io.ReadAll(r.Body)
				defer r.Body.Close()

				require.NoError(t, err)

				var requestBody *twofactorverify.RequestBody
				require.NoError(t, json.Unmarshal(b, &requestBody))

				switch requestBody.KeyId {
				case "verify_via_push":
					body := map[string]interface{}{
						"success": true,
					}
					json.NewEncoder(w).Encode(body)
				case "verify_via_otp_with_push_error":
					w.WriteHeader(http.StatusInternalServerError)
				default:
					<-waitInfinitely
				}
			},
		},
	}

	return requests
}

const errorHeader = "OTP validation failed: "

func TestExecute(t *testing.T) {
	requests := setup(t)

	url := testserver.StartSocketHttpServer(t, requests)

	testCases := []struct {
		desc           string
		arguments      *commandargs.Shell
		input          io.Reader
		expectedOutput string
	}{
		{
			desc:           "Verify via OTP",
			arguments:      &commandargs.Shell{GitlabKeyId: "verify_via_otp"},
			expectedOutput: "OTP validation successful. Git operations are now allowed.\n",
		},
		{
			desc:           "Verify via OTP",
			arguments:      &commandargs.Shell{GitlabKeyId: "verify_via_otp_with_push_error"},
			expectedOutput: "OTP validation successful. Git operations are now allowed.\n",
		},
		{
			desc:           "Verify via push authentication",
			arguments:      &commandargs.Shell{GitlabKeyId: "verify_via_push"},
			input:          &blockingReader{},
			expectedOutput: "OTP has been validated by Push Authentication. Git operations are now allowed.\n",
		},
		{
			desc:           "With an empty OTP",
			arguments:      &commandargs.Shell{GitlabKeyId: "verify_via_otp"},
			input:          bytes.NewBufferString("\n"),
			expectedOutput: errorHeader + "OTP cannot be blank.\n",
		},
		{
			desc:           "With bad response",
			arguments:      &commandargs.Shell{GitlabKeyId: "-1"},
			expectedOutput: errorHeader + "Parsing failed\n",
		},
		{
			desc:           "With API returns an error",
			arguments:      &commandargs.Shell{GitlabKeyId: "error"},
			expectedOutput: errorHeader + "error message\n",
		},
		{
			desc:           "With API fails",
			arguments:      &commandargs.Shell{GitlabKeyId: "broken"},
			expectedOutput: errorHeader + "Internal API unreachable\n",
		},
		{
			desc:           "With missing arguments",
			arguments:      &commandargs.Shell{},
			expectedOutput: errorHeader + "who='' is invalid\n",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.desc, func(t *testing.T) {
			output := &bytes.Buffer{}

			input := tc.input
			if input == nil {
				input = bytes.NewBufferString("123456\n")
			}

			cmd := &Command{
				Config:     &config.Config{GitlabUrl: url},
				Args:       tc.arguments,
				ReadWriter: &readwriter.ReadWriter{Out: output, In: input},
			}

			_, err := cmd.Execute(context.Background())

			require.NoError(t, err)
			require.Equal(t, prompt+"\n"+tc.expectedOutput, output.String())
		})
	}
}

func TestCanceledContext(t *testing.T) {
	requests := setup(t)

	output := &bytes.Buffer{}

	url := testserver.StartSocketHttpServer(t, requests)
	cmd := &Command{
		Config:     &config.Config{GitlabUrl: url},
		Args:       &commandargs.Shell{GitlabKeyId: "wait_infinitely"},
		ReadWriter: &readwriter.ReadWriter{Out: output, In: &blockingReader{}},
	}

	ctx, cancel := context.WithCancel(context.Background())

	errCh := make(chan error)
	go func() {
		_, err := cmd.Execute(ctx)
		errCh <- err
	}()
	cancel()

	require.NoError(t, <-errCh)
	require.Equal(t, prompt+"\n"+errorHeader+"context canceled\n", output.String())
}
