// Copyright 2023 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package gitea

import (
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"golang.org/x/crypto/ssh"
)

func TestParsedPaging(t *testing.T) {
	resp := newResponse(&http.Response{
		Header: http.Header{
			"Link": []string{
				strings.Join(
					[]string{
						`<https://try.gitea.io/api/v1/repos/gitea/go-sdk/issues/1/comments?page=3>; rel="next"`,
						`<https://try.gitea.io/api/v1/repos/gitea/go-sdk/issues/1/comments?page=4>; rel="last"`,
						`<https://try.gitea.io/api/v1/repos/gitea/go-sdk/issues/1/comments?page=1>; rel="first"`,
						`<https://try.gitea.io/api/v1/repos/gitea/go-sdk/issues/1/comments?page=1>; rel="prev"`,
					}, ",",
				),
			},
		},
	})

	assert.Equal(t, 1, resp.FirstPage)
	assert.Equal(t, 1, resp.PrevPage)
	assert.Equal(t, 3, resp.NextPage)
	assert.Equal(t, 4, resp.LastPage)
}

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

	var requests atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests.Add(1)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(`{}`))
	}))
	defer server.Close()

	client, err := NewClient(server.URL, SetGiteaVersion("1.26.0"))
	assert.NoError(t, err)

	ctx, cancel := context.WithCancel(t.Context())
	cancel()

	_, _, err = client.GetMyUserInfo(ctx)
	assert.ErrorIs(t, err, context.Canceled)
	assert.Zero(t, requests.Load())

	_, _, err = client.GetMyUserInfo(t.Context())
	assert.False(t, errors.Is(err, context.Canceled))
	assert.NoError(t, err)
	assert.EqualValues(t, 1, requests.Load())
}

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

	var versionRequests atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/api/v1/version" {
			versionRequests.Add(1)
		}
		w.WriteHeader(http.StatusNoContent)
	}))
	defer server.Close()

	client, err := NewClient(server.URL, SetGiteaVersion("1.26.0"))
	assert.NoError(t, err)

	err = client.CheckServerVersionConstraint(t.Context(), ">= 1.11.0")
	assert.NoError(t, err)
	assert.Zero(t, versionRequests.Load())
}

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

	var versionRequests atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/api/v1/version" {
			versionRequests.Add(1)
		}
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(`{"version":"1.26.0"}`))
	}))
	defer server.Close()

	_, err := NewClient(server.URL)
	assert.NoError(t, err)
	assert.Zero(t, versionRequests.Load())
}

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

	_, privateKey, err := ed25519.GenerateKey(rand.Reader)
	assert.NoError(t, err)

	signer, err := ssh.NewSignerFromKey(privateKey)
	assert.NoError(t, err)

	var versionRequests atomic.Int32
	var signedVersionRequests atomic.Int32
	var signedAPIRequests atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/api/v1/version":
			versionRequests.Add(1)
			if r.Header.Get("Signature") != "" {
				signedVersionRequests.Add(1)
			}
			w.Header().Set("Content-Type", "application/json")
			_, _ = w.Write([]byte(`{"version":"1.26.0"}`))
		case "/api/v1/user":
			if r.Header.Get("Signature") != "" {
				signedAPIRequests.Add(1)
			}
			w.Header().Set("Content-Type", "application/json")
			_, _ = w.Write([]byte(`{}`))
		default:
			t.Fatalf("unexpected path %s", r.URL.Path)
		}
	}))
	defer server.Close()

	client, err := NewClient(server.URL, SetHTTPClient(server.Client()))
	assert.NoError(t, err)
	client.httpsigner = &HTTPSign{Signer: signer}

	_, _, err = client.GetMyUserInfo(t.Context())
	assert.NoError(t, err)
	_, _, err = client.GetMyUserInfo(t.Context())
	assert.NoError(t, err)
	assert.EqualValues(t, 1, versionRequests.Load())
	assert.Zero(t, signedVersionRequests.Load())
	assert.EqualValues(t, 2, signedAPIRequests.Load())
}

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

	_, privateKey, err := ed25519.GenerateKey(rand.Reader)
	assert.NoError(t, err)

	signer, err := ssh.NewSignerFromKey(privateKey)
	assert.NoError(t, err)

	var signedVersionRequests atomic.Int32
	var unsignedVersionRequests atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/api/v1/version" {
			t.Fatalf("unexpected path %s", r.URL.Path)
		}
		if r.Header.Get("Signature") != "" {
			signedVersionRequests.Add(1)
		} else {
			unsignedVersionRequests.Add(1)
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"version":"1.26.0"}`))
	}))
	defer server.Close()

	client, err := NewClient(server.URL, SetHTTPClient(server.Client()))
	assert.NoError(t, err)
	client.httpsigner = &HTTPSign{Signer: signer}

	rawVersion, _, err := client.Meta.ServerVersion(t.Context())
	assert.NoError(t, err)
	assert.Equal(t, "1.26.0", rawVersion)
	assert.EqualValues(t, 1, unsignedVersionRequests.Load())
	assert.EqualValues(t, 1, signedVersionRequests.Load())
}

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

	_, privateKey, err := ed25519.GenerateKey(rand.Reader)
	assert.NoError(t, err)

	signer, err := ssh.NewSignerFromKey(privateKey)
	assert.NoError(t, err)

	var versionRequests atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/api/v1/version":
			versionRequests.Add(1)
			w.Header().Set("Content-Type", "application/json")
			_, _ = w.Write([]byte(`{"version":"1.26.0"}`))
		case "/api/v1/user":
			w.Header().Set("Content-Type", "application/json")
			_, _ = w.Write([]byte(`{}`))
		default:
			t.Fatalf("unexpected path %s", r.URL.Path)
		}
	}))
	defer server.Close()

	client, err := NewClient(server.URL, SetHTTPClient(server.Client()))
	assert.NoError(t, err)
	client.httpsigner = &HTTPSign{Signer: signer}

	done := make(chan error, 1)
	go func() {
		_, _, err := client.GetMyUserInfo(t.Context())
		done <- err
	}()

	select {
	case err := <-done:
		assert.NoError(t, err)
	case <-time.After(2 * time.Second):
		t.Fatal("GetMyUserInfo deadlocked while bootstrapping the server version with HTTPSign enabled")
	}

	assert.EqualValues(t, 1, versionRequests.Load())
}
