package api

import (
	"errors"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/gotify/server/v2/auth/password"
	"github.com/gotify/server/v2/mode"
	"github.com/gotify/server/v2/model"
	"github.com/gotify/server/v2/test"
	"github.com/gotify/server/v2/test/testdb"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/suite"
)

func TestUserSuite(t *testing.T) {
	suite.Run(t, new(UserSuite))
}

type UserSuite struct {
	suite.Suite
	db             *testdb.Database
	a              *UserAPI
	ctx            *gin.Context
	recorder       *httptest.ResponseRecorder
	notifiedAdd    bool
	notifiedDelete bool
	notifier       *UserChangeNotifier
}

func (s *UserSuite) BeforeTest(suiteName, testName string) {
	mode.Set(mode.TestDev)
	s.recorder = httptest.NewRecorder()
	s.ctx, _ = gin.CreateTestContext(s.recorder)
	s.db = testdb.NewDB(s.T())
	s.notifier = new(UserChangeNotifier)
	s.notifier.OnUserDeleted(func(uint) error {
		s.notifiedDelete = true
		return nil
	})
	s.notifier.OnUserAdded(func(uint) error {
		s.notifiedAdd = true
		return nil
	})
	s.a = &UserAPI{DB: s.db, UserChangeNotifier: s.notifier}
}
func (s *UserSuite) AfterTest(suiteName, testName string) {
	s.db.Close()
}

func (s *UserSuite) Test_GetUsers() {
	first := s.db.NewUser(2)
	second := s.db.NewUser(5)

	s.a.GetUsers(s.ctx)

	assert.Equal(s.T(), 200, s.recorder.Code)
	test.BodyEquals(s.T(), []*model.UserExternal{externalOf(first), externalOf(second)}, s.recorder)
}

func (s *UserSuite) Test_GetCurrentUser() {
	user := s.db.NewUser(5)

	test.WithUser(s.ctx, 5)
	s.a.GetCurrentUser(s.ctx)

	assert.Equal(s.T(), 200, s.recorder.Code)
	test.BodyEquals(s.T(), externalOf(user), s.recorder)
}

func (s *UserSuite) Test_GetUserByID() {
	user := s.db.NewUser(2)

	s.ctx.Params = gin.Params{{Key: "id", Value: "2"}}

	s.a.GetUserByID(s.ctx)

	assert.Equal(s.T(), 200, s.recorder.Code)
	test.BodyEquals(s.T(), externalOf(user), s.recorder)
}

func (s *UserSuite) Test_GetUserByID_InvalidID() {
	s.db.User(2)
	s.ctx.Params = gin.Params{{Key: "id", Value: "abc"}}

	s.a.GetUserByID(s.ctx)

	assert.Equal(s.T(), 400, s.recorder.Code)
}

func (s *UserSuite) Test_GetUserByID_UnknownUser() {
	s.db.User(2)

	s.ctx.Params = gin.Params{{Key: "id", Value: "3"}}

	s.a.GetUserByID(s.ctx)

	assert.Equal(s.T(), 404, s.recorder.Code)
}

func (s *UserSuite) Test_DeleteUserByID_LastAdmin_Expect400() {
	s.db.CreateUser(&model.User{
		ID:    7,
		Name:  "admin",
		Admin: true,
	})
	s.ctx.Params = gin.Params{{Key: "id", Value: "7"}}

	s.a.DeleteUserByID(s.ctx)

	assert.Equal(s.T(), 400, s.recorder.Code)
}

func (s *UserSuite) Test_DeleteUserByID_InvalidID() {
	s.ctx.Params = gin.Params{{Key: "id", Value: "abc"}}

	s.a.DeleteUserByID(s.ctx)

	assert.Equal(s.T(), 400, s.recorder.Code)
}

func (s *UserSuite) Test_DeleteUserByID_UnknownUser() {
	s.db.User(2)

	s.ctx.Params = gin.Params{{Key: "id", Value: "3"}}

	s.a.DeleteUserByID(s.ctx)

	assert.Equal(s.T(), 404, s.recorder.Code)
}

func (s *UserSuite) Test_DeleteUserByID() {
	assert.False(s.T(), s.notifiedDelete)

	s.db.User(2)

	s.ctx.Params = gin.Params{{Key: "id", Value: "2"}}

	s.a.DeleteUserByID(s.ctx)

	assert.Equal(s.T(), 200, s.recorder.Code)
	s.db.AssertUserNotExist(2)
	assert.True(s.T(), s.notifiedDelete)
}

func (s *UserSuite) Test_DeleteUserByID_NotifyFail() {
	s.db.User(5)
	s.notifier.OnUserDeleted(func(id uint) error {
		if id == 5 {
			return errors.New("some error")
		}
		return nil
	})

	s.ctx.Params = gin.Params{{Key: "id", Value: "5"}}

	s.a.DeleteUserByID(s.ctx)

	assert.Equal(s.T(), 500, s.recorder.Code)
}

func (s *UserSuite) Test_CreateUser() {
	assert.False(s.T(), s.notifiedAdd)
	s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "mylittlepony", "admin": true}`))
	s.ctx.Request.Header.Set("Content-Type", "application/json")

	s.a.CreateUser(s.ctx)

	user := &model.UserExternal{ID: 1, Name: "tom", Admin: true}
	test.BodyEquals(s.T(), user, s.recorder)
	assert.Equal(s.T(), 200, s.recorder.Code)

	if created, err := s.db.GetUserByName("tom"); assert.NoError(s.T(), err) {
		assert.NotNil(s.T(), created)
		assert.True(s.T(), password.ComparePassword(created.Pass, []byte("mylittlepony")))
	}
	assert.True(s.T(), s.notifiedAdd)
}

func (s *UserSuite) Test_CreateUser_NotifyFail() {
	s.notifier.OnUserAdded(func(id uint) error {
		user, err := s.db.GetUserByID(id)
		if err != nil {
			return err
		}
		if user.Name == "eva" {
			return errors.New("some error")
		}
		return nil
	})
	s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "eva", "pass": "mylittlepony", "admin": true}`))
	s.ctx.Request.Header.Set("Content-Type", "application/json")

	s.a.CreateUser(s.ctx)

	assert.Equal(s.T(), 500, s.recorder.Code)
}

func (s *UserSuite) Test_CreateUser_NoPassword() {
	s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "", "admin": true}`))
	s.ctx.Request.Header.Set("Content-Type", "application/json")

	s.a.CreateUser(s.ctx)

	assert.Equal(s.T(), 400, s.recorder.Code)
}

func (s *UserSuite) Test_CreateUser_NoName() {
	s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "", "pass": "asd", "admin": true}`))
	s.ctx.Request.Header.Set("Content-Type", "application/json")

	s.a.CreateUser(s.ctx)

	assert.Equal(s.T(), 400, s.recorder.Code)
}

func (s *UserSuite) Test_CreateUser_NameAlreadyExists() {
	s.db.NewUserWithName(1, "tom")

	s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "mylittlepony", "admin": true}`))
	s.ctx.Request.Header.Set("Content-Type", "application/json")

	s.a.CreateUser(s.ctx)

	assert.Equal(s.T(), 400, s.recorder.Code)
}

func (s *UserSuite) Test_UpdateUserByID_InvalidID() {
	s.ctx.Params = gin.Params{{Key: "id", Value: "abc"}}

	s.ctx.Request = httptest.NewRequest("POST", "/user/abc", strings.NewReader(`{"name": "tom", "pass": "", "admin": false}`))
	s.ctx.Request.Header.Set("Content-Type", "application/json")

	s.a.UpdateUserByID(s.ctx)

	assert.Equal(s.T(), 400, s.recorder.Code)
}

func (s *UserSuite) Test_UpdateUserByID_LastAdmin_Expect400() {
	s.db.CreateUser(&model.User{
		ID:    7,
		Name:  "admin",
		Admin: true,
	})

	s.ctx.Params = gin.Params{{Key: "id", Value: "7"}}

	s.ctx.Request = httptest.NewRequest("POST", "/user/7", strings.NewReader(`{"name": "admin", "pass": "", "admin": false}`))
	s.ctx.Request.Header.Set("Content-Type", "application/json")
	s.a.UpdateUserByID(s.ctx)

	assert.Equal(s.T(), 400, s.recorder.Code)
}

func (s *UserSuite) Test_UpdateUserByID_UnknownUser() {
	s.ctx.Params = gin.Params{{Key: "id", Value: "2"}}

	s.ctx.Request = httptest.NewRequest("POST", "/user/2", strings.NewReader(`{"name": "tom", "pass": "", "admin": false}`))
	s.ctx.Request.Header.Set("Content-Type", "application/json")

	s.a.UpdateUserByID(s.ctx)

	assert.Equal(s.T(), 404, s.recorder.Code)
}

func (s *UserSuite) Test_UpdateUserByID_UpdateNotPassword() {
	s.db.CreateUser(&model.User{ID: 2, Name: "nico", Pass: password.CreatePassword("old", 5)})

	s.ctx.Params = gin.Params{{Key: "id", Value: "2"}}

	s.ctx.Request = httptest.NewRequest("POST", "/user/2", strings.NewReader(`{"name": "tom", "pass": "", "admin": true}`))
	s.ctx.Request.Header.Set("Content-Type", "application/json")

	s.a.UpdateUserByID(s.ctx)

	assert.Equal(s.T(), 200, s.recorder.Code)
	user, err := s.db.GetUserByID(2)
	assert.NoError(s.T(), err)
	assert.NotNil(s.T(), user)
	assert.True(s.T(), password.ComparePassword(user.Pass, []byte("old")))
}

func (s *UserSuite) Test_UpdateUserByID_UpdatePassword() {
	s.db.CreateUser(&model.User{ID: 2, Name: "tom", Pass: password.CreatePassword("old", 5)})

	s.ctx.Params = gin.Params{{Key: "id", Value: "2"}}

	s.ctx.Request = httptest.NewRequest("POST", "/user/2", strings.NewReader(`{"name": "tom", "pass": "new", "admin": true}`))
	s.ctx.Request.Header.Set("Content-Type", "application/json")

	s.a.UpdateUserByID(s.ctx)

	assert.Equal(s.T(), 200, s.recorder.Code)
	user, err := s.db.GetUserByID(2)
	assert.NoError(s.T(), err)
	assert.NotNil(s.T(), user)
	assert.True(s.T(), password.ComparePassword(user.Pass, []byte("new")))
}

func (s *UserSuite) Test_UpdatePassword() {
	s.db.CreateUser(&model.User{ID: 1, Name: "jmattheis", Pass: password.CreatePassword("old", 5)})

	test.WithUser(s.ctx, 1)
	s.ctx.Request = httptest.NewRequest("POST", "/user/current/password", strings.NewReader(`{"pass": "new"}`))
	s.ctx.Request.Header.Set("Content-Type", "application/json")

	s.a.ChangePassword(s.ctx)

	assert.Equal(s.T(), 200, s.recorder.Code)
	user, err := s.db.GetUserByID(1)
	assert.NoError(s.T(), err)
	assert.NotNil(s.T(), user)
	assert.True(s.T(), password.ComparePassword(user.Pass, []byte("new")))
}

func (s *UserSuite) Test_UpdatePassword_EmptyPassword() {
	s.db.CreateUser(&model.User{ID: 1, Name: "jmattheis", Pass: password.CreatePassword("old", 5)})

	test.WithUser(s.ctx, 1)
	s.ctx.Request = httptest.NewRequest("POST", "/user/current/password", strings.NewReader(`{"pass":""}`))
	s.ctx.Request.Header.Set("Content-Type", "application/json")

	s.a.ChangePassword(s.ctx)

	assert.Equal(s.T(), 400, s.recorder.Code)
	user, err := s.db.GetUserByID(1)
	assert.NoError(s.T(), err)
	assert.NotNil(s.T(), user)
	assert.True(s.T(), password.ComparePassword(user.Pass, []byte("old")))
}

func externalOf(user *model.User) *model.UserExternal {
	return &model.UserExternal{Name: user.Name, Admin: user.Admin, ID: user.ID}
}
