// Copyright (C) 2019-2022  Nicola Murino
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package mfa

import (
	"testing"
	"time"

	"github.com/pquerna/otp"
	"github.com/pquerna/otp/totp"
	"github.com/stretchr/testify/assert"
)

func TestMFAConfig(t *testing.T) {
	config := Config{
		TOTP: []TOTPConfig{
			{},
		},
	}
	configName1 := "config1"
	configName2 := "config2"
	configName3 := "config3"
	err := config.Initialize()
	assert.Error(t, err)
	config.TOTP[0].Name = configName1
	err = config.Initialize()
	assert.Error(t, err)
	config.TOTP[0].Issuer = "issuer"
	err = config.Initialize()
	assert.Error(t, err)
	config.TOTP[0].Algo = TOTPAlgoSHA1
	err = config.Initialize()
	assert.NoError(t, err)
	config.TOTP = append(config.TOTP, TOTPConfig{
		Name:   configName1,
		Issuer: "SFTPGo",
		Algo:   TOTPAlgoSHA512,
	})
	err = config.Initialize()
	assert.Error(t, err)
	config.TOTP[1].Name = configName2
	err = config.Initialize()
	assert.NoError(t, err)
	assert.Len(t, GetAvailableTOTPConfigs(), 2)
	assert.Len(t, GetAvailableTOTPConfigNames(), 2)
	config.TOTP = append(config.TOTP, TOTPConfig{
		Name:   configName3,
		Issuer: "SFTPGo",
		Algo:   TOTPAlgoSHA256,
	})
	err = config.Initialize()
	assert.NoError(t, err)
	assert.Len(t, GetAvailableTOTPConfigs(), 3)
	if assert.Len(t, GetAvailableTOTPConfigNames(), 3) {
		assert.Contains(t, GetAvailableTOTPConfigNames(), configName1)
		assert.Contains(t, GetAvailableTOTPConfigNames(), configName2)
		assert.Contains(t, GetAvailableTOTPConfigNames(), configName3)
	}
	status := GetStatus()
	assert.True(t, status.IsActive)
	if assert.Len(t, status.TOTPConfigs, 3) {
		assert.Equal(t, configName1, status.TOTPConfigs[0].Name)
		assert.Equal(t, configName2, status.TOTPConfigs[1].Name)
		assert.Equal(t, configName3, status.TOTPConfigs[2].Name)
	}
	// now generate some secrets and validate some passcodes
	_, _, _, _, err = GenerateTOTPSecret("", "") //nolint:dogsled
	assert.Error(t, err)
	match, err := ValidateTOTPPasscode("", "", "")
	assert.Error(t, err)
	assert.False(t, match)
	cfgName, _, secret, _, err := GenerateTOTPSecret(configName1, "user1")
	assert.NoError(t, err)
	assert.NotEmpty(t, secret)
	assert.Equal(t, configName1, cfgName)
	passcode, err := generatePasscode(secret, otp.AlgorithmSHA1)
	assert.NoError(t, err)
	match, err = ValidateTOTPPasscode(configName1, passcode, secret)
	assert.NoError(t, err)
	assert.True(t, match)
	match, err = ValidateTOTPPasscode(configName1, passcode, secret)
	assert.ErrorIs(t, err, errPasscodeUsed)
	assert.False(t, match)

	passcode, err = generatePasscode(secret, otp.AlgorithmSHA256)
	assert.NoError(t, err)
	// config1 uses sha1 algo
	match, err = ValidateTOTPPasscode(configName1, passcode, secret)
	assert.NoError(t, err)
	assert.False(t, match)
	// config3 use the expected algo
	match, err = ValidateTOTPPasscode(configName3, passcode, secret)
	assert.NoError(t, err)
	assert.True(t, match)

	stopCleanupTicker()
}

func TestCleanupPasscodes(t *testing.T) {
	usedPasscodes.Store("key", time.Now().Add(-24*time.Hour).UTC())
	startCleanupTicker(30 * time.Millisecond)
	assert.Eventually(t, func() bool {
		_, ok := usedPasscodes.Load("key")
		return !ok
	}, 1000*time.Millisecond, 100*time.Millisecond)
	stopCleanupTicker()
}

func TestTOTPGenerateErrors(t *testing.T) {
	config := TOTPConfig{
		Name:   "name",
		Issuer: "",
		algo:   otp.AlgorithmSHA1,
	}
	// issuer cannot be empty
	_, _, _, err := config.generate("username", 200, 200) //nolint:dogsled
	assert.Error(t, err)
	config.Issuer = "issuer"
	// we cannot encode an image smaller than 45x45
	_, _, _, err = config.generate("username", 30, 30) //nolint:dogsled
	assert.Error(t, err)
}

func generatePasscode(secret string, algo otp.Algorithm) (string, error) {
	return totp.GenerateCodeCustom(secret, time.Now(), totp.ValidateOpts{
		Period:    30,
		Skew:      1,
		Digits:    otp.DigitsSix,
		Algorithm: algo,
	})
}
