package api

import (
	"bytes"
	"context"
	"io"
	"net/http"
	"testing"
	"time"

	"github.com/cli/go-gh/v2/internal/testutils"
	"github.com/stretchr/testify/assert"
	"gopkg.in/h2non/gock.v1"
)

func TestRESTClient(t *testing.T) {
	testutils.StubConfig(t, testConfig())
	t.Cleanup(gock.Off)

	gock.New("https://api.github.com").
		Get("/some/test/path").
		MatchHeader("Authorization", "token abc123").
		Reply(200).
		JSON(`{"message": "success"}`)

	client, err := DefaultRESTClient()
	assert.NoError(t, err)

	res := struct{ Message string }{}
	err = client.Do("GET", "some/test/path", nil, &res)
	assert.NoError(t, err)
	assert.True(t, gock.IsDone(), printPendingMocks(gock.Pending()))
	assert.Equal(t, "success", res.Message)
}

func TestRESTClientRequest(t *testing.T) {
	tests := []struct {
		name       string
		host       string
		path       string
		httpMocks  func()
		wantErr    bool
		wantErrMsg string
		wantBody   string
	}{
		{
			name: "success request empty response",
			path: "some/test/path",
			httpMocks: func() {
				gock.New("https://api.github.com").
					Get("/some/test/path").
					Reply(204)
			},
			wantBody: ``,
		},
		{
			name: "success request non-empty response",
			path: "some/test/path",
			httpMocks: func() {
				gock.New("https://api.github.com").
					Get("/some/test/path").
					Reply(200).
					JSON(`{"message": "success"}`)
			},
			wantBody: `{"message": "success"}`,
		},
		{
			name: "fail request empty response",
			path: "some/test/path",
			httpMocks: func() {
				gock.New("https://api.github.com").
					Get("/some/test/path").
					Reply(404).
					JSON(`{}`)
			},
			wantErr:    true,
			wantErrMsg: "HTTP 404 (https://api.github.com/some/test/path)",
			wantBody:   `{}`,
		},
		{
			name: "fail request non-empty response",
			path: "some/test/path",
			httpMocks: func() {
				gock.New("https://api.github.com").
					Get("/some/test/path").
					Reply(422).
					JSON(`{"message": "OH NO"}`)
			},
			wantErr:    true,
			wantErrMsg: "HTTP 422: OH NO (https://api.github.com/some/test/path)",
			wantBody:   `{"message": "OH NO"}`,
		},
		{
			name: "support full urls",
			path: "https://example.com/someother/test/path",
			httpMocks: func() {
				gock.New("https://example.com").
					Get("/someother/test/path").
					Reply(200).
					JSON(`{}`)
			},
			wantBody: `{}`,
		},
		{
			name: "support enterprise hosts",
			host: "enterprise.com",
			path: "some/test/path",
			httpMocks: func() {
				gock.New("https://enterprise.com").
					Get("/some/test/path").
					Reply(200).
					JSON(`{}`)
			},
			wantBody: `{}`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Cleanup(gock.Off)
			if tt.host == "" {
				tt.host = "github.com"
			}
			if tt.httpMocks != nil {
				tt.httpMocks()
			}
			client, _ := NewRESTClient(ClientOptions{
				Host:      tt.host,
				AuthToken: "token",
				Transport: http.DefaultTransport,
			})

			resp, err := client.Request("GET", tt.path, nil)
			if tt.wantErr {
				assert.EqualError(t, err, tt.wantErrMsg)
			} else {
				assert.NoError(t, err)
			}

			if err == nil {
				defer resp.Body.Close()
				body, err := io.ReadAll(resp.Body)
				assert.NoError(t, err)
				assert.Equal(t, tt.wantBody, string(body))
			}

			assert.True(t, gock.IsDone(), printPendingMocks(gock.Pending()))
		})
	}
}

func TestRESTClientDo(t *testing.T) {
	tests := []struct {
		name       string
		host       string
		path       string
		httpMocks  func()
		wantErr    bool
		wantErrMsg string
		wantMsg    string
	}{
		{
			name: "success request empty response",
			path: "some/test/path",
			httpMocks: func() {
				gock.New("https://api.github.com").
					Get("/some/test/path").
					Reply(204).
					JSON(`{}`)
			},
		},
		{
			name: "success request non-empty response",
			path: "some/test/path",
			httpMocks: func() {
				gock.New("https://api.github.com").
					Get("/some/test/path").
					Reply(200).
					JSON(`{"message": "success"}`)
			},
			wantMsg: "success",
		},
		{
			name: "fail request empty response",
			path: "some/test/path",
			httpMocks: func() {
				gock.New("https://api.github.com").
					Get("/some/test/path").
					Reply(404).
					JSON(`{}`)
			},
			wantErr:    true,
			wantErrMsg: "HTTP 404 (https://api.github.com/some/test/path)",
		},
		{
			name: "fail request non-empty response",
			path: "some/test/path",
			httpMocks: func() {
				gock.New("https://api.github.com").
					Get("/some/test/path").
					Reply(422).
					JSON(`{"message": "OH NO"}`)
			},
			wantErr:    true,
			wantErrMsg: "HTTP 422: OH NO (https://api.github.com/some/test/path)",
		},
		{
			name: "support full urls",
			path: "https://example.com/someother/test/path",
			httpMocks: func() {
				gock.New("https://example.com").
					Get("/someother/test/path").
					Reply(204).
					JSON(`{}`)
			},
		},
		{
			name: "support enterprise hosts",
			host: "enterprise.com",
			path: "some/test/path",
			httpMocks: func() {
				gock.New("https://enterprise.com").
					Get("/some/test/path").
					Reply(204).
					JSON(`{}`)
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Cleanup(gock.Off)
			if tt.host == "" {
				tt.host = "github.com"
			}
			if tt.httpMocks != nil {
				tt.httpMocks()
			}
			client, _ := NewRESTClient(ClientOptions{
				Host:      tt.host,
				AuthToken: "token",
				Transport: http.DefaultTransport,
			})
			res := struct{ Message string }{}
			err := client.Do("GET", tt.path, nil, &res)
			if tt.wantErr {
				assert.EqualError(t, err, tt.wantErrMsg)
			} else {
				assert.NoError(t, err)
			}
			assert.True(t, gock.IsDone(), printPendingMocks(gock.Pending()))
			assert.Equal(t, tt.wantMsg, res.Message)
		})
	}
}

func TestRESTClientDelete(t *testing.T) {
	t.Cleanup(gock.Off)
	gock.New("https://api.github.com").
		Delete("/some/path/here").
		Reply(204).
		JSON(`{}`)
	client, _ := NewRESTClient(ClientOptions{
		Host:      "github.com",
		AuthToken: "token",
		Transport: http.DefaultTransport,
	})
	err := client.Delete("some/path/here", nil)
	assert.NoError(t, err)
	assert.True(t, gock.IsDone(), printPendingMocks(gock.Pending()))
}

func TestRESTClientGet(t *testing.T) {
	t.Cleanup(gock.Off)
	gock.New("https://api.github.com").
		Get("/some/path/here").
		Reply(204).
		JSON(`{}`)
	client, _ := NewRESTClient(ClientOptions{
		Host:      "github.com",
		AuthToken: "token",
		Transport: http.DefaultTransport,
	})
	err := client.Get("some/path/here", nil)
	assert.NoError(t, err)
	assert.True(t, gock.IsDone(), printPendingMocks(gock.Pending()))
}

func TestRESTClientPatch(t *testing.T) {
	t.Cleanup(gock.Off)
	gock.New("https://api.github.com").
		Patch("/some/path/here").
		BodyString(`{}`).
		Reply(204).
		JSON(`{}`)
	client, _ := NewRESTClient(ClientOptions{
		Host:      "github.com",
		AuthToken: "token",
		Transport: http.DefaultTransport,
	})
	r := bytes.NewReader([]byte(`{}`))
	err := client.Patch("some/path/here", r, nil)
	assert.NoError(t, err)
	assert.True(t, gock.IsDone(), printPendingMocks(gock.Pending()))
}

func TestRESTClientPost(t *testing.T) {
	t.Cleanup(gock.Off)
	gock.New("https://api.github.com").
		Post("/some/path/here").
		BodyString(`{}`).
		Reply(204).
		JSON(`{}`)
	client, _ := NewRESTClient(ClientOptions{
		Host:      "github.com",
		AuthToken: "token",
		Transport: http.DefaultTransport,
	})
	r := bytes.NewReader([]byte(`{}`))
	err := client.Post("some/path/here", r, nil)
	assert.NoError(t, err)
	assert.True(t, gock.IsDone(), printPendingMocks(gock.Pending()))
}

func TestRESTClientPut(t *testing.T) {
	t.Cleanup(gock.Off)
	gock.New("https://api.github.com").
		Put("/some/path/here").
		BodyString(`{}`).
		Reply(204).
		JSON(`{}`)
	client, _ := NewRESTClient(ClientOptions{
		Host:      "github.com",
		AuthToken: "token",
		Transport: http.DefaultTransport,
	})
	r := bytes.NewReader([]byte(`{}`))
	err := client.Put("some/path/here", r, nil)
	assert.NoError(t, err)
	assert.True(t, gock.IsDone(), printPendingMocks(gock.Pending()))
}

func TestRESTClientDoWithContext(t *testing.T) {
	tests := []struct {
		name       string
		wantErrMsg string
		getCtx     func() context.Context
	}{
		{
			name: "http fail request canceled",
			getCtx: func() context.Context {
				ctx, cancel := context.WithCancel(context.Background())
				// call 'cancel' to ensure that context is already canceled
				cancel()
				return ctx
			},
			wantErrMsg: `Get "https://api.github.com/some/path": context canceled`,
		},
		{
			name: "http fail request timed out",
			getCtx: func() context.Context {
				// pass current time to ensure that deadline has already passed
				ctx, cancel := context.WithDeadline(context.Background(), time.Now())
				cancel()
				return ctx
			},
			wantErrMsg: `Get "https://api.github.com/some/path": context deadline exceeded`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// given
			t.Cleanup(gock.Off)
			gock.New("https://api.github.com").
				Get("/some/path").
				Reply(204).
				JSON(`{}`)

			client, _ := NewRESTClient(ClientOptions{
				Host:      "github.com",
				AuthToken: "token",
				Transport: http.DefaultTransport,
			})
			res := struct{ Message string }{}

			// when
			ctx := tt.getCtx()
			gotErr := client.DoWithContext(ctx, http.MethodGet, "some/path", nil, &res)

			// then
			assert.EqualError(t, gotErr, tt.wantErrMsg)
			assert.True(t, gock.IsDone(), printPendingMocks(gock.Pending()))
		})
	}
}

func TestRESTClientRequestWithContext(t *testing.T) {
	tests := []struct {
		name       string
		wantErrMsg string
		getCtx     func() context.Context
	}{
		{
			name: "http fail request canceled",
			getCtx: func() context.Context {
				ctx, cancel := context.WithCancel(context.Background())
				// call 'cancel' to ensure that context is already canceled
				cancel()
				return ctx
			},
			wantErrMsg: `Get "https://api.github.com/some/path": context canceled`,
		},
		{
			name: "http fail request timed out",
			getCtx: func() context.Context {
				// pass current time to ensure that deadline has already passed
				ctx, cancel := context.WithDeadline(context.Background(), time.Now())
				cancel()
				return ctx
			},
			wantErrMsg: `Get "https://api.github.com/some/path": context deadline exceeded`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// given
			t.Cleanup(gock.Off)
			gock.New("https://api.github.com").
				Get("/some/path").
				Reply(204).
				JSON(`{}`)

			client, _ := NewRESTClient(ClientOptions{
				Host:      "github.com",
				AuthToken: "token",
				Transport: http.DefaultTransport,
			})

			// when
			ctx := tt.getCtx()
			_, gotErr := client.RequestWithContext(ctx, http.MethodGet, "some/path", nil)

			// then
			assert.EqualError(t, gotErr, tt.wantErrMsg)
			assert.True(t, gock.IsDone(), printPendingMocks(gock.Pending()))
		})
	}
}

func TestRestPrefix(t *testing.T) {
	tests := []struct {
		name         string
		host         string
		wantEndpoint string
	}{
		{
			name:         "github",
			host:         "github.com",
			wantEndpoint: "https://api.github.com/",
		},
		{
			name:         "localhost",
			host:         "github.localhost",
			wantEndpoint: "http://api.github.localhost/",
		},
		{
			name:         "garage",
			host:         "garage.github.com",
			wantEndpoint: "https://garage.github.com/api/v3/",
		},
		{
			name:         "enterprise",
			host:         "enterprise.com",
			wantEndpoint: "https://enterprise.com/api/v3/",
		},
		{
			name:         "tenant",
			host:         "tenant.ghe.com",
			wantEndpoint: "https://api.tenant.ghe.com/",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			endpoint := restPrefix(tt.host)
			assert.Equal(t, tt.wantEndpoint, endpoint)
		})
	}
}
