// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge.  If not, see <https://www.gnu.org/licenses/>.

package credentials

import (
	"bytes"
	"encoding/base64"
	"encoding/gob"
	"encoding/json"
	"fmt"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const testSep = "\n"
const secretFormat = "%v" + testSep + // UserID,
	"%v" + testSep + // Name,
	"%v" + testSep + // Emails,
	"%v" + testSep + // APIToken,
	"%v" + testSep + // Mailbox,
	"%v" + testSep + // BridgePassword,
	"%v" + testSep + // Version string
	"%v" + testSep + // Timestamp,
	"%v" + testSep + // IsHidden,
	"%v" // IsCombinedAddressMode

// the best would be to run this test on mac, win, and linux separately

type testCredentials struct {
	UserID,
	Name,
	Emails,
	APIToken,
	Mailbox,
	BridgePassword,
	Version string
	Timestamp int64
	IsHidden,
	IsCombinedAddressMode bool
}

func init() { //nolint[gochecknoinits]
	gob.Register(testCredentials{})
}

func (s *testCredentials) MarshalGob() string {
	buf := bytes.Buffer{}
	enc := gob.NewEncoder(&buf)
	if err := enc.Encode(s); err != nil {
		return ""
	}
	fmt.Printf("MarshalGob: %#v\n", buf.String())
	return base64.StdEncoding.EncodeToString(buf.Bytes())
}

func (s *testCredentials) Clear() {
	s.UserID = ""
	s.Name = ""
	s.Emails = ""
	s.APIToken = ""
	s.Mailbox = ""
	s.BridgePassword = ""
	s.Version = ""
	s.Timestamp = 0
	s.IsHidden = false
	s.IsCombinedAddressMode = false
}

func (s *testCredentials) UnmarshalGob(secret string) error {
	s.Clear()
	b, err := base64.StdEncoding.DecodeString(secret)
	if err != nil {
		fmt.Println("decode base64", b)
		return err
	}
	buf := bytes.NewBuffer(b)
	dec := gob.NewDecoder(buf)
	if err = dec.Decode(s); err != nil {
		fmt.Println("decode gob", b, buf.Bytes())
		return err
	}
	return nil
}

func (s *testCredentials) ToJSON() string {
	if b, err := json.Marshal(s); err == nil {
		fmt.Printf("MarshalJSON: %#v\n", string(b))
		return base64.StdEncoding.EncodeToString(b)
	}
	return ""
}

func (s *testCredentials) FromJSON(secret string) error {
	b, err := base64.StdEncoding.DecodeString(secret)
	if err != nil {
		return err
	}
	if err = json.Unmarshal(b, s); err == nil {
		return nil
	}
	return err
}

func (s *testCredentials) MarshalFmt() string {
	buf := bytes.Buffer{}
	fmt.Fprintf(
		&buf, secretFormat,
		s.UserID,
		s.Name,
		s.Emails,
		s.APIToken,
		s.Mailbox,
		s.BridgePassword,
		s.Version,
		s.Timestamp,
		s.IsHidden,
		s.IsCombinedAddressMode,
	)
	fmt.Printf("MarshalFmt: %#v\n", buf.String())
	return base64.StdEncoding.EncodeToString(buf.Bytes())
}

func (s *testCredentials) UnmarshalFmt(secret string) error {
	b, err := base64.StdEncoding.DecodeString(secret)
	if err != nil {
		return err
	}
	buf := bytes.NewBuffer(b)
	fmt.Println("decode fmt", b, buf.Bytes())
	_, err = fmt.Fscanf(
		buf, secretFormat,
		&s.UserID,
		&s.Name,
		&s.Emails,
		&s.APIToken,
		&s.Mailbox,
		&s.BridgePassword,
		&s.Version,
		&s.Timestamp,
		&s.IsHidden,
		&s.IsCombinedAddressMode,
	)
	if err != nil {
		return err
	}
	return nil
}

func (s *testCredentials) MarshalStrings() string { // this is the most space efficient
	items := []string{
		s.UserID,         // 0
		s.Name,           // 1
		s.Emails,         // 2
		s.APIToken,       // 3
		s.Mailbox,        // 4
		s.BridgePassword, // 5
		s.Version,        // 6
	}
	items = append(items, fmt.Sprint(s.Timestamp)) // 7

	if s.IsHidden { // 8
		items = append(items, "1")
	} else {
		items = append(items, "")
	}

	if s.IsCombinedAddressMode { // 9
		items = append(items, "1")
	} else {
		items = append(items, "")
	}

	str := strings.Join(items, sep)

	fmt.Printf("MarshalJoin: %#v\n", str)
	return base64.StdEncoding.EncodeToString([]byte(str))
}

func (s *testCredentials) UnmarshalStrings(secret string) error {
	b, err := base64.StdEncoding.DecodeString(secret)
	if err != nil {
		return err
	}
	items := strings.Split(string(b), sep)
	if len(items) != 10 {
		return ErrWrongFormat
	}

	s.UserID = items[0]
	s.Name = items[1]
	s.Emails = items[2]
	s.APIToken = items[3]
	s.Mailbox = items[4]
	s.BridgePassword = items[5]
	s.Version = items[6]
	if _, err = fmt.Sscanf(items[7], "%d", &s.Timestamp); err != nil {
		s.Timestamp = 0
	}
	if s.IsHidden = false; items[8] == "1" {
		s.IsHidden = true
	}
	if s.IsCombinedAddressMode = false; items[9] == "1" {
		s.IsCombinedAddressMode = true
	}
	return nil
}

func (s *testCredentials) IsSame(rhs *testCredentials) bool {
	return s.Name == rhs.Name &&
		s.Emails == rhs.Emails &&
		s.APIToken == rhs.APIToken &&
		s.Mailbox == rhs.Mailbox &&
		s.BridgePassword == rhs.BridgePassword &&
		s.Version == rhs.Version &&
		s.Timestamp == rhs.Timestamp &&
		s.IsHidden == rhs.IsHidden &&
		s.IsCombinedAddressMode == rhs.IsCombinedAddressMode
}

func TestMarshalFormats(t *testing.T) {
	input := testCredentials{UserID: "007", Emails: "ja@pm.me;jakub@cu.th", Timestamp: 152469263742, IsHidden: true}
	fmt.Printf("input %#v\n", input)

	secretStrings := input.MarshalStrings()
	fmt.Printf("secretStrings %#v %d\n", secretStrings, len(secretStrings))
	secretGob := input.MarshalGob()
	fmt.Printf("secretGob %#v %d\n", secretGob, len(secretGob))
	secretJSON := input.ToJSON()
	fmt.Printf("secretJSON %#v %d\n", secretJSON, len(secretJSON))
	secretFmt := input.MarshalFmt()
	fmt.Printf("secretFmt %#v %d\n", secretFmt, len(secretFmt))

	output := testCredentials{APIToken: "refresh"}
	require.NoError(t, output.UnmarshalStrings(secretStrings))
	fmt.Printf("strings out %#v \n", output)
	require.True(t, input.IsSame(&output), "strings out not same")

	output = testCredentials{APIToken: "refresh"}
	require.NoError(t, output.UnmarshalGob(secretGob))
	fmt.Printf("gob out %#v\n \n", output)
	assert.Equal(t, input, output)

	output = testCredentials{APIToken: "refresh"}
	require.NoError(t, output.FromJSON(secretJSON))
	fmt.Printf("json out %#v \n", output)
	require.True(t, input.IsSame(&output), "json out not same")

	/*
		// Simple Fscanf not working!
		output = testCredentials{APIToken: "refresh"}
			require.NoError(t, output.UnmarshalFmt(secretFmt))
			fmt.Printf("fmt out %#v \n", output)
			require.True(t, input.IsSame(&output), "fmt out not same")
	*/
}

func TestMarshal(t *testing.T) {
	input := Credentials{
		UserID:                "",
		Name:                  "007",
		Emails:                "ja@pm.me;aj@cus.tom",
		APIToken:              "sdfdsfsdfsdfsdf",
		MailboxPassword:       "cdcdcdcd",
		BridgePassword:        "wew123",
		Version:               "k11",
		Timestamp:             152469263742,
		IsHidden:              true,
		IsCombinedAddressMode: false,
	}
	fmt.Printf("input %#v\n", input)

	secret := input.Marshal()
	fmt.Printf("secret %#v %d\n", secret, len(secret))

	output := Credentials{APIToken: "refresh"}
	require.NoError(t, output.Unmarshal(secret))
	fmt.Printf("output %#v\n", output)
	assert.Equal(t, input, output)
}
