// Copyright 2020 The Gogs 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 lfs

import (
	"bytes"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gopkg.in/macaron.v1"

	"gogs.io/gogs/internal/database"
	"gogs.io/gogs/internal/lfsutil"
)

var _ lfsutil.Storager = (*mockStorage)(nil)

// mockStorage is an in-memory storage for LFS objects.
type mockStorage struct {
	buf *bytes.Buffer
}

func (*mockStorage) Storage() lfsutil.Storage {
	return "memory"
}

func (s *mockStorage) Upload(_ lfsutil.OID, rc io.ReadCloser) (int64, error) {
	defer func() { _ = rc.Close() }()
	return io.Copy(s.buf, rc)
}

func (s *mockStorage) Download(_ lfsutil.OID, w io.Writer) error {
	_, err := io.Copy(w, s.buf)
	return err
}

func TestBasicHandler_serveDownload(t *testing.T) {
	s := &mockStorage{}
	basic := &basicHandler{
		defaultStorage: s.Storage(),
		storagers: map[lfsutil.Storage]lfsutil.Storager{
			s.Storage(): s,
		},
	}

	m := macaron.New()
	m.Use(macaron.Renderer())
	m.Use(func(c *macaron.Context) {
		c.Map(&database.Repository{Name: "repo"})
		c.Map(lfsutil.OID("ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"))
	})
	m.Get("/", basic.serveDownload)

	tests := []struct {
		name          string
		content       string
		mockStore     func() *MockStore
		expStatusCode int
		expHeader     http.Header
		expBody       string
	}{
		{
			name: "object does not exist",
			mockStore: func() *MockStore {
				mockStore := NewMockStore()
				mockStore.GetLFSObjectByOIDFunc.SetDefaultReturn(nil, database.ErrLFSObjectNotExist{})
				return mockStore
			},
			expStatusCode: http.StatusNotFound,
			expHeader: http.Header{
				"Content-Type": []string{"application/vnd.git-lfs+json"},
			},
			expBody: `{"message":"Object does not exist"}` + "\n",
		},
		{
			name: "storage not found",
			mockStore: func() *MockStore {
				mockStore := NewMockStore()
				mockStore.GetLFSObjectByOIDFunc.SetDefaultReturn(&database.LFSObject{Storage: "bad_storage"}, nil)
				return mockStore
			},
			expStatusCode: http.StatusInternalServerError,
			expHeader: http.Header{
				"Content-Type": []string{"application/vnd.git-lfs+json"},
			},
			expBody: `{"message":"Internal server error"}` + "\n",
		},

		{
			name:    "object exists",
			content: "Hello world!",
			mockStore: func() *MockStore {
				mockStore := NewMockStore()
				mockStore.GetLFSObjectByOIDFunc.SetDefaultReturn(
					&database.LFSObject{
						Size:    12,
						Storage: s.Storage(),
					},
					nil,
				)
				return mockStore
			},
			expStatusCode: http.StatusOK,
			expHeader: http.Header{
				"Content-Type":   []string{"application/octet-stream"},
				"Content-Length": []string{"12"},
			},
			expBody: "Hello world!",
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			basic.store = test.mockStore()

			s.buf = bytes.NewBufferString(test.content)

			r, err := http.NewRequest(http.MethodGet, "/", nil)
			require.NoError(t, err)

			rr := httptest.NewRecorder()
			m.ServeHTTP(rr, r)

			resp := rr.Result()
			assert.Equal(t, test.expStatusCode, resp.StatusCode)
			assert.Equal(t, test.expHeader, resp.Header)

			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)
			assert.Equal(t, test.expBody, string(body))
		})
	}
}

func TestBasicHandler_serveUpload(t *testing.T) {
	s := &mockStorage{buf: &bytes.Buffer{}}
	basic := &basicHandler{
		defaultStorage: s.Storage(),
		storagers: map[lfsutil.Storage]lfsutil.Storager{
			s.Storage(): s,
		},
	}

	m := macaron.New()
	m.Use(macaron.Renderer())
	m.Use(func(c *macaron.Context) {
		c.Map(&database.Repository{Name: "repo"})
		c.Map(lfsutil.OID("ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"))
	})
	m.Put("/", basic.serveUpload)

	tests := []struct {
		name          string
		mockStore     func() *MockStore
		expStatusCode int
		expBody       string
	}{
		{
			name: "object already exists",
			mockStore: func() *MockStore {
				mockStore := NewMockStore()
				mockStore.GetLFSObjectByOIDFunc.SetDefaultReturn(&database.LFSObject{}, nil)
				return mockStore
			},
			expStatusCode: http.StatusOK,
		},
		{
			name: "new object",
			mockStore: func() *MockStore {
				mockStore := NewMockStore()
				mockStore.GetLFSObjectByOIDFunc.SetDefaultReturn(nil, database.ErrLFSObjectNotExist{})
				return mockStore
			},
			expStatusCode: http.StatusOK,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			basic.store = test.mockStore()

			r, err := http.NewRequest("PUT", "/", strings.NewReader("Hello world!"))
			require.NoError(t, err)

			rr := httptest.NewRecorder()
			m.ServeHTTP(rr, r)

			resp := rr.Result()
			assert.Equal(t, test.expStatusCode, resp.StatusCode)

			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)
			assert.Equal(t, test.expBody, string(body))
		})
	}
}

func TestBasicHandler_serveVerify(t *testing.T) {
	basic := &basicHandler{}

	m := macaron.New()
	m.Use(macaron.Renderer())
	m.Use(func(c *macaron.Context) {
		c.Map(&database.Repository{Name: "repo"})
	})
	m.Post("/", basic.serveVerify)

	tests := []struct {
		name          string
		body          string
		mockStore     func() *MockStore
		expStatusCode int
		expBody       string
	}{
		{
			name:          "invalid oid",
			body:          `{"oid": "bad_oid"}`,
			expStatusCode: http.StatusBadRequest,
			expBody:       `{"message":"Invalid oid"}` + "\n",
		},
		{
			name: "object does not exist",
			body: `{"oid":"ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"}`,
			mockStore: func() *MockStore {
				mockStore := NewMockStore()
				mockStore.GetLFSObjectByOIDFunc.SetDefaultReturn(nil, database.ErrLFSObjectNotExist{})
				return mockStore
			},
			expStatusCode: http.StatusNotFound,
			expBody:       `{"message":"Object does not exist"}` + "\n",
		},
		{
			name: "object size mismatch",
			body: `{"oid":"ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"}`,
			mockStore: func() *MockStore {
				mockStore := NewMockStore()
				mockStore.GetLFSObjectByOIDFunc.SetDefaultReturn(&database.LFSObject{Size: 12}, nil)
				return mockStore
			},
			expStatusCode: http.StatusBadRequest,
			expBody:       `{"message":"Object size mismatch"}` + "\n",
		},

		{
			name: "object exists",
			body: `{"oid":"ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f", "size":12}`,
			mockStore: func() *MockStore {
				mockStore := NewMockStore()
				mockStore.GetLFSObjectByOIDFunc.SetDefaultReturn(&database.LFSObject{Size: 12}, nil)
				return mockStore
			},
			expStatusCode: http.StatusOK,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if test.mockStore != nil {
				basic.store = test.mockStore()
			}

			r, err := http.NewRequest("POST", "/", strings.NewReader(test.body))
			require.NoError(t, err)

			rr := httptest.NewRecorder()
			m.ServeHTTP(rr, r)

			resp := rr.Result()
			assert.Equal(t, test.expStatusCode, resp.StatusCode)

			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)
			assert.Equal(t, test.expBody, string(body))
		})
	}
}