package basicauth

import (
	"errors"
	"os"
	"reflect"
	"testing"

	"github.com/kataras/iris/v12/context"

	"golang.org/x/crypto/bcrypt"
	"gopkg.in/yaml.v3"
)

type IUserRepository interface {
	GetByUsernameAndPassword(dest interface{}, username, password string) error
}

// Test a custom implementation of AuthFunc with a user repository.
// This is a usage example of custom AuthFunc implementation.
func UserRepository(repo IUserRepository, newUserPtr func() interface{}) AuthFunc {
	return func(_ *context.Context, username, password string) (interface{}, bool) {
		dest := newUserPtr()
		err := repo.GetByUsernameAndPassword(dest, username, password)
		if err == nil {
			return dest, true
		}

		return nil, false
	}
}

type testUser struct {
	username string
	password string
	email    string // custom field.
}

// GetUsername & Getpassword complete the User interface.
func (u *testUser) GetUsername() string {
	return u.username
}

func (u *testUser) GetPassword() string {
	return u.password
}

type testRepo struct {
	entries []testUser
}

// Implements IUserRepository interface.
func (r *testRepo) GetByUsernameAndPassword(dest interface{}, username, password string) error {
	for _, e := range r.entries {
		if e.username == username && e.password == password {
			*dest.(*testUser) = e
			return nil
		}
	}

	return errors.New("invalid credentials")
}

func TestAllowUserRepository(t *testing.T) {
	repo := &testRepo{
		entries: []testUser{
			{username: "kataras", password: "kataras_pass", email: "kataras2006@hotmail.com"},
		},
	}

	allow := UserRepository(repo, func() interface{} {
		return new(testUser)
	})

	var tests = []struct {
		username string
		password string
		ok       bool
		user     *testUser
	}{
		{
			username: "kataras",
			password: "kataras_pass",
			ok:       true,
			user:     &testUser{username: "kataras", password: "kataras_pass", email: "kataras2006@hotmail.com"},
		},
		{
			username: "makis",
			password: "makis_password",
			ok:       false,
		},
	}

	for i, tt := range tests {
		v, ok := allow(nil, tt.username, tt.password)

		if tt.ok != ok {
			t.Fatalf("[%d] expected: %v but got: %v (username=%s,password=%s)", i, tt.ok, ok, tt.username, tt.password)
		}

		if !ok {
			continue
		}

		u, ok := v.(*testUser)
		if !ok {
			t.Fatalf("[%d] a user should be type of *testUser but got: %#+v (%T)", i, v, v)
		}

		if !reflect.DeepEqual(tt.user, u) {
			t.Fatalf("[%d] expected user:\n%#+v\nbut got:\n%#+v", i, tt.user, u)
		}
	}
}

func TestAllowUsers(t *testing.T) {
	users := []User{
		&testUser{username: "kataras", password: "kataras_pass", email: "kataras2006@hotmail.com"},
	}

	allow := AllowUsers(users)

	var tests = []struct {
		username string
		password string
		ok       bool
		user     *testUser
	}{
		{
			username: "kataras",
			password: "kataras_pass",
			ok:       true,
			user:     &testUser{username: "kataras", password: "kataras_pass", email: "kataras2006@hotmail.com"},
		},
		{
			username: "makis",
			password: "makis_password",
			ok:       false,
		},
	}

	for i, tt := range tests {
		v, ok := allow(nil, tt.username, tt.password)

		if tt.ok != ok {
			t.Fatalf("[%d] expected: %v but got: %v (username=%s,password=%s)", i, tt.ok, ok, tt.username, tt.password)
		}

		if !ok {
			continue
		}

		u, ok := v.(*testUser)
		if !ok {
			t.Fatalf("[%d] a user should be type of *testUser but got: %#+v (%T)", i, v, v)
		}

		if !reflect.DeepEqual(tt.user, u) {
			t.Fatalf("[%d] expected user:\n%#+v\nbut got:\n%#+v", i, tt.user, u)
		}
	}
}

// Test YAML user loading with b-encrypted passwords.
func TestAllowUsersFile(t *testing.T) {
	f, err := os.CreateTemp("", "*users.yml")
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		f.Close()
		os.Remove(f.Name())
	}()

	// 	f.WriteString(`
	// - username: kataras
	//   password: kataras_pass
	//   age: 27
	//   role: admin
	// - username: makis
	//   password: makis_password
	// `)
	// This form is supported too, although its features are limited (no custom fields):
	// 	f.WriteString(`
	// kataras: kataras_pass
	// makis: makis_password
	// `)

	var tests = []struct {
		username      string
		password      string // hashed, auto-filled later on.
		inputPassword string
		ok            bool
		user          context.Map
	}{
		{
			username:      "kataras",
			inputPassword: "kataras_pass",
			ok:            true,
			user:          context.Map{"age": 27, "role": "admin"}, // username and password are auto-filled in our tests below.
		},
		{
			username:      "makis",
			inputPassword: "makis_password",
			ok:            true,
			user:          context.Map{},
		},
		{
			username: "invalid",
			password: "invalid_pass",
			ok:       false,
		},
		{
			username: "notvalid",
			password: "",
			ok:       false,
		},
	}

	// Write the tests to the users YAML file.
	var usersToWrite []context.Map
	for _, tt := range tests {
		if tt.ok {
			// store the hashed password.
			tt.password = mustGeneratePassword(t, tt.inputPassword)

			// store and write the username and hashed password.
			tt.user["username"] = tt.username
			tt.user["password"] = tt.password

			// cannot write it as a stream, write it as a slice.
			// enc.Encode(tt.user)
			usersToWrite = append(usersToWrite, tt.user)
		}
		// 	bcrypt.GenerateFromPassword([]byte("kataras_pass"), bcrypt.DefaultCost)
	}

	fileContents, err := yaml.Marshal(usersToWrite)
	if err != nil {
		t.Fatal(err)
	}
	f.Write(fileContents)

	// Build the authentication func.
	allow := AllowUsersFile(f.Name(), BCRYPT)
	for i, tt := range tests {
		v, ok := allow(nil, tt.username, tt.inputPassword)

		if tt.ok != ok {
			t.Fatalf("[%d] expected: %v but got: %v (username=%s,password=%s,user=%#+v)", i, tt.ok, ok, tt.username, tt.inputPassword, v)
		}

		if !ok {
			continue
		}

		if len(tt.user) == 0 { // when username: password form.
			continue
		}

		u, ok := v.(context.Map)
		if !ok {
			t.Fatalf("[%d] a user loaded from external source or file should be alway type of map[string]interface{} but got: %#+v (%T)", i, v, v)
		}

		if expected, got := len(tt.user), len(u); expected != got {
			t.Fatalf("[%d] expected user map length to be equal, expected: %d but got: %d\n%#+v\n%#+v", i, expected, got, tt.user, u)
		}

		for k, v := range tt.user {
			if u[k] != v {
				t.Fatalf("[%d] expected user map %q to be %q but got: %q", i, k, v, u[k])
			}
		}
	}

}

func mustGeneratePassword(t *testing.T, userPassword string) string {
	t.Helper()
	hashed, err := bcrypt.GenerateFromPassword([]byte(userPassword), bcrypt.DefaultCost)
	if err != nil {
		t.Fatal(err)
	}

	return string(hashed)
}
