package gitlab

import (
	"encoding/json"
	"net/http"
	"strings"
	"testing"

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

type testUser struct {
	ID   int64  `json:"id"`
	Name string `json:"name"`
}

type testProject struct {
	ID          int64  `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description"`
}

func TestDoRequestSuccess(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN
	path := "/api/v4/users/1"
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		mustWriteHTTPResponse(t, w, "testdata/get_user.json")
	})

	// WHEN
	user, resp, err := do[*testUser](
		client,
		withPath("users/1"),
	)

	// THEN
	assert.NoError(t, err)
	assert.Equal(t, 200, resp.StatusCode)
	assert.Equal(t, int64(1), user.ID)
	assert.Equal(t, "John Smith", user.Name)
}

func TestDoRequestPOSTWithBody(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN
	path := "/api/v4/projects"
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodPost)

		var reqBody testProject
		err := json.NewDecoder(r.Body).Decode(&reqBody)
		assert.NoError(t, err)
		assert.Equal(t, "New Project", reqBody.Name)

		w.WriteHeader(http.StatusCreated)
		w.Header().Set("Content-Type", "application/json")
		w.Write([]byte(`{"id": 1, "name": "New Project", "description": "Test project"}`))
	})

	requestBody := &testProject{
		Name:        "New Project",
		Description: "Test project",
	}

	// WHEN
	project, resp, err := do[*testProject](
		client,
		withMethod(http.MethodPost),
		withPath("projects"),
		withAPIOpts(requestBody),
	)

	// THEN
	assert.NoError(t, err)
	assert.Equal(t, 201, resp.StatusCode)
	assert.Equal(t, "New Project", project.Name)
}

func TestDoRequestErrorResponse(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN
	path := "/api/v4/users/999"
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		w.WriteHeader(http.StatusNotFound)
		w.Write([]byte(`{"message": "Not found"}`))
	})

	// WHEN
	user, resp, err := do[*testUser](
		client,
		withPath("users/99"),
	)

	// THEN
	assert.Error(t, err)
	assert.Equal(t, 404, resp.StatusCode)
	assert.Nil(t, user)
}

func TestDoRequestSliceSuccess(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN
	path := "/api/v4/users"
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		mustWriteHTTPResponse(t, w, "testdata/list_users.json")
	})

	// WHEN
	users, resp, err := do[[]testUser](
		client,
		withPath("users"),
	)

	// THEN
	assert.NoError(t, err)
	assert.Equal(t, 200, resp.StatusCode)
	assert.Len(t, users, 3)

	expectedUsers := []testUser{
		{ID: 1, Name: "Example User 1"},
		{ID: 2, Name: "Example User 2"},
		{ID: 3, Name: "Example User 3"},
	}
	for i, user := range users {
		assert.Equal(t, expectedUsers[i].ID, user.ID)
		assert.Equal(t, expectedUsers[i].Name, user.Name)
	}
}

func TestDoRequestSliceEmptySlice(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN
	path := "/api/v4/users"
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		w.Header().Set("Content-Type", "application/json")
		w.Write([]byte("[]"))
	})

	// WHEN
	users, resp, err := do[[]testUser](
		client,
		withPath("users"),
	)

	// THEN
	assert.NoError(t, err)
	assert.Equal(t, 200, resp.StatusCode)
	assert.Empty(t, users)
}

func TestDoRequestSliceErrorResponse(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN
	path := "/api/v4/users"
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte(`{"message": "Internal server error"}`))
	})

	// WHEN
	users, resp, err := do[[]testUser](
		client,
		withPath("users"),
	)

	// THEN
	assert.Error(t, err)
	assert.Equal(t, 500, resp.StatusCode)
	assert.Nil(t, users)
}

func TestDoRequestVoidSuccessDELETE(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN
	path := "/api/v4/users/1"
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodDelete)
		w.WriteHeader(http.StatusNoContent)
	})

	// WHEN
	_, resp, err := do[none](
		client,
		withMethod(http.MethodDelete),
		withPath("users/1"),
	)

	// THEN
	assert.NoError(t, err)
	assert.Equal(t, 204, resp.StatusCode)
}

func TestDoRequestVoidSuccessPUT(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN
	path := "/api/v4/merge_requests/1/approve"
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodPut)

		var reqBody map[string]any
		err := json.NewDecoder(r.Body).Decode(&reqBody)
		assert.NoError(t, err)
		assert.Equal(t, "approve", reqBody["action"])

		w.WriteHeader(http.StatusOK)
	})

	requestBody := map[string]string{"action": "approve"}

	// WHEN
	_, resp, err := do[none](
		client,
		withMethod(http.MethodPut),
		withPath("merge_requests/1/approve"),
		withAPIOpts(requestBody),
	)

	// THEN
	assert.NoError(t, err)
	assert.Equal(t, 200, resp.StatusCode)
}

func TestDoRequestVoidErrorResponse(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN
	path := "/api/v4/users/1"
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodDelete)
		w.WriteHeader(http.StatusForbidden)
		w.Write([]byte(`{"message": "Forbidden"}`))
	})

	// WHEN
	_, resp, err := do[none](
		client,
		withMethod(http.MethodDelete),
		withPath("users/1"),
	)

	// THEN
	assert.Error(t, err)
	assert.Equal(t, 403, resp.StatusCode)
}

func TestRequestHandlerWithOptions(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN
	path := "/api/v4/users"
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		// Check that query parameters from options are included
		assert.Equal(t, "2", r.URL.Query().Get("page"))
		assert.Equal(t, "test-value", r.Header.Get("X-Test-Header"))

		mustWriteHTTPResponse(t, w, "testdata/list_users_public_email.json")
	})

	options := []RequestOptionFunc{
		WithOffsetPaginationParameters(2),
		WithHeader("X-Test-Header", "test-value"),
	}

	// WHEN
	users, resp, err := do[[]testUser](
		client,
		withPath("users"),
		withRequestOpts(options...),
	)

	// THEN
	assert.NoError(t, err)
	assert.Equal(t, 200, resp.StatusCode)
	assert.Len(t, users, 1)
}

func TestDoRequestProjectID(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN
	mux.HandleFunc("/api/v4/projects/group%2Fproject", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	// WHEN
	_, resp, err := do[none](
		client,
		withPath("projects/%s", ProjectID{"group/project"}),
	)

	// THEN
	assert.NoError(t, err)
	assert.Equal(t, 200, resp.StatusCode)
}

func TestDoRequestGroupID(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN
	mux.HandleFunc("/api/v4/groups/sub%2Fgroup", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	// WHEN
	_, resp, err := do[none](
		client,
		withPath("groups/%s", GroupID{"sub/group"}),
	)

	// THEN
	assert.NoError(t, err)
	assert.Equal(t, 200, resp.StatusCode)
}

func TestDoRequestRunnerID(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN
	mux.HandleFunc("/api/v4/runners/some%2Frunner", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	// WHEN
	_, resp, err := do[none](
		client,
		withPath("runners/%s", RunnerID{"some/runner"}),
	)

	// THEN
	assert.NoError(t, err)
	assert.Equal(t, 200, resp.StatusCode)
}

func TestDoRequestUserID(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name         string
		uid          any
		expectedPath string
	}{
		{
			name:         "numeric user ID",
			uid:          123,
			expectedPath: "/api/v4/users/123/status",
		},
		{
			name:         "username string",
			uid:          "johndoe",
			expectedPath: "/api/v4/users/johndoe/status",
		},
		{
			name:         "username with @ prefix is trimmed",
			uid:          "@johndoe",
			expectedPath: "/api/v4/users/johndoe/status",
		},
		{
			name:         "username with slash is escaped",
			uid:          "test/user",
			expectedPath: "/api/v4/users/test%2Fuser/status",
		},
		{
			name:         "username with @ prefix and slash",
			uid:          "@test/user",
			expectedPath: "/api/v4/users/test%2Fuser/status",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			mux, client := setup(t)

			// GIVEN
			mux.HandleFunc(tt.expectedPath, func(w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(http.StatusOK)
			})

			// WHEN
			_, resp, err := do[none](
				client,
				withPath("users/%s/status", UserID{tt.uid}),
			)

			// THEN
			assert.NoError(t, err)
			assert.Equal(t, 200, resp.StatusCode)
		})
	}
}

func TestDoRequestUserIDInvalidType(t *testing.T) {
	t.Parallel()
	_, client := setup(t)

	// WHEN
	_, _, err := do[none](
		client,
		withPath("users/%s/status", UserID{struct{ ID int }{ID: 1}}),
	)

	// THEN
	assert.ErrorIs(t, err, ErrInvalidIDType)
}

func TestDoRequestUploadSuccess(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN
	path := "/api/v4/projects/1/uploads"
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodPost)
		assert.Contains(t, r.Header.Get("Content-Type"), "multipart/form-data;")
		assert.NotEqual(t, int64(-1), r.ContentLength)
		w.WriteHeader(http.StatusCreated)
		w.Write([]byte(`{"id": 1, "name": "test.txt"}`))
	})

	content := strings.NewReader("file content")

	// WHEN
	type uploadResult struct {
		ID   int64  `json:"id"`
		Name string `json:"name"`
	}
	result, resp, err := do[*uploadResult](
		client,
		withMethod(http.MethodPost),
		withPath("projects/1/uploads"),
		withUpload(content, "test.txt", UploadFile),
	)

	// THEN
	assert.NoError(t, err)
	assert.Equal(t, 201, resp.StatusCode)
	assert.Equal(t, int64(1), result.ID)
	assert.Equal(t, "test.txt", result.Name)
}

func TestDoRequestUploadWithAPIOpts(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN
	path := "/api/v4/projects/1/wikis/attachments"
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodPost)
		assert.Contains(t, r.Header.Get("Content-Type"), "multipart/form-data;")

		err := r.ParseMultipartForm(32 << 20)
		assert.NoError(t, err)
		assert.Equal(t, "main", r.FormValue("branch"))

		w.WriteHeader(http.StatusCreated)
		w.Write([]byte(`{"file_name": "test.png"}`))
	})

	content := strings.NewReader("image data")
	opts := struct {
		Branch string `url:"branch" json:"branch"`
	}{Branch: "main"}

	// WHEN
	type uploadResult struct {
		FileName string `json:"file_name"`
	}
	result, resp, err := do[*uploadResult](
		client,
		withMethod(http.MethodPost),
		withPath("projects/1/wikis/attachments"),
		withUpload(content, "test.png", UploadFile),
		withAPIOpts(opts),
	)

	// THEN
	assert.NoError(t, err)
	assert.Equal(t, 201, resp.StatusCode)
	assert.Equal(t, "test.png", result.FileName)
}

func TestDoRequestUploadAvatar(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN
	path := "/api/v4/projects/1"
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodPut)
		assert.Contains(t, r.Header.Get("Content-Type"), "multipart/form-data;")

		err := r.ParseMultipartForm(32 << 20)
		assert.NoError(t, err)

		_, header, err := r.FormFile("avatar")
		assert.NoError(t, err)
		assert.Equal(t, "avatar.png", header.Filename)

		w.WriteHeader(http.StatusOK)
		w.Write([]byte(`{"id": 1}`))
	})

	content := strings.NewReader("avatar data")

	// WHEN
	result, resp, err := do[*testProject](
		client,
		withMethod(http.MethodPut),
		withPath("projects/1"),
		withUpload(content, "avatar.png", UploadAvatar),
	)

	// THEN
	assert.NoError(t, err)
	assert.Equal(t, 200, resp.StatusCode)
	assert.Equal(t, int64(1), result.ID)
}

func TestDoRequestUploadErrorResponse(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN
	path := "/api/v4/projects/1/uploads"
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodPost)
		w.WriteHeader(http.StatusRequestEntityTooLarge)
		w.Write([]byte(`{"message": "File too large"}`))
	})

	content := strings.NewReader("file content")

	// WHEN
	type uploadResult struct {
		ID int64 `json:"id"`
	}
	result, resp, err := do[*uploadResult](
		client,
		withMethod(http.MethodPost),
		withPath("projects/1/uploads"),
		withUpload(content, "large.txt", UploadFile),
	)

	// THEN
	assert.Error(t, err)
	assert.Equal(t, 413, resp.StatusCode)
	assert.Nil(t, result)
}

func TestDoRequestUploadWithProjectID(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN
	mux.HandleFunc("/api/v4/projects/group%2Fproject/uploads", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodPost)
		assert.Contains(t, r.Header.Get("Content-Type"), "multipart/form-data;")
		w.WriteHeader(http.StatusCreated)
		w.Write([]byte(`{"id": 1}`))
	})

	content := strings.NewReader("file content")

	// WHEN
	type uploadResult struct {
		ID int64 `json:"id"`
	}
	result, resp, err := do[*uploadResult](
		client,
		withMethod(http.MethodPost),
		withPath("projects/%s/uploads", ProjectID{"group/project"}),
		withUpload(content, "test.txt", UploadFile),
	)

	// THEN
	assert.NoError(t, err)
	assert.Equal(t, 201, resp.StatusCode)
	assert.Equal(t, int64(1), result.ID)
}
