// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.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 tests

import (
	"fmt"
	"io"
	"io/ioutil"
	"net/mail"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"github.com/cucumber/godog"
	"github.com/cucumber/godog/gherkin"
	"github.com/emersion/go-mbox"
	"github.com/emersion/go-message"
	"github.com/pkg/errors"
	a "github.com/stretchr/testify/assert"
)

func TransferChecksFeatureContext(s *godog.Suite) {
	s.Step(`^progress result is "([^"]*)"$`, progressFinishedWith)
	s.Step(`^transfer exported (\d+) messages$`, transferExportedNumberOfMessages)
	s.Step(`^transfer imported (\d+) messages$`, transferImportedNumberOfMessages)
	s.Step(`^transfer failed for (\d+) messages$`, transferFailedForNumberOfMessages)
	s.Step(`^transfer exported messages$`, transferExportedMessages)
	s.Step(`^exported messages match the original ones$`, exportedMessagesMatchTheOriginalOnes)
}

func progressFinishedWith(wantResponse string) error {
	progress := ctx.GetTransferProgress()
	// Wait till transport is finished.
	for range progress.GetUpdateChannel() {
	}

	err := progress.GetFatalError()
	if wantResponse == "OK" {
		a.NoError(ctx.GetTestingT(), err)
	} else {
		a.EqualError(ctx.GetTestingT(), err, wantResponse)
	}
	return ctx.GetTestingError()
}

func transferExportedNumberOfMessages(wantCount int) error {
	progress := ctx.GetTransferProgress()
	_, _, exported, _, _ := progress.GetCounts() //nolint[dogsled]
	a.Equal(ctx.GetTestingT(), uint(wantCount), exported)
	return ctx.GetTestingError()
}

func transferImportedNumberOfMessages(wantCount int) error {
	progress := ctx.GetTransferProgress()
	_, imported, _, _, _ := progress.GetCounts() //nolint[dogsled]
	a.Equal(ctx.GetTestingT(), uint(wantCount), imported)
	return ctx.GetTestingError()
}

func transferFailedForNumberOfMessages(wantCount int) error {
	progress := ctx.GetTransferProgress()
	failedMessages := progress.GetFailedMessages()
	a.Equal(ctx.GetTestingT(), wantCount, len(failedMessages), "failed messages: %v", failedMessages)
	return ctx.GetTestingError()
}

func transferExportedMessages(messages *gherkin.DataTable) error {
	expectedMessages := map[string][]MessageAttributes{}

	head := messages.Rows[0].Cells
	for _, row := range messages.Rows[1:] {
		folder := ""
		msg := MessageAttributes{}

		for n, cell := range row.Cells {
			switch head[n].Value {
			case "folder":
				folder = cell.Value
			case "subject":
				msg.subject = cell.Value
			case "from":
				msg.from = cell.Value
			case "to":
				msg.to = []string{cell.Value}
			case "time":
				date, err := time.Parse(timeFormat, cell.Value)
				if err != nil {
					return internalError(err, "failed to parse time")
				}
				msg.date = date.Unix()
			default:
				return fmt.Errorf("unexpected column name: %s", head[n].Value)
			}
		}

		expectedMessages[folder] = append(expectedMessages[folder], msg)
		sort.Sort(BySubject(expectedMessages[folder]))
	}

	exportRoot := ctx.GetTransferLocalRootForExport()
	exportedMessages, err := readMessages(exportRoot)
	if err != nil {
		return errors.Wrap(err, "scanning exported messages")
	}

	a.Equal(ctx.GetTestingT(), expectedMessages, exportedMessages)
	return ctx.GetTestingError()
}

func exportedMessagesMatchTheOriginalOnes() error {
	importRoot := ctx.GetTransferLocalRootForImport()
	exportRoot := ctx.GetTransferLocalRootForExport()

	importMessages, err := readMessages(importRoot)
	if err != nil {
		return errors.Wrap(err, "scanning messages for import")
	}
	exportMessages, err := readMessages(exportRoot)
	if err != nil {
		return errors.Wrap(err, "scanning exported messages")
	}
	delete(exportMessages, "All Mail") // Ignore All Mail.

	a.Equal(ctx.GetTestingT(), importMessages, exportMessages)
	return ctx.GetTestingError()
}

func readMessages(root string) (map[string][]MessageAttributes, error) {
	files, err := ioutil.ReadDir(root)
	if err != nil {
		return nil, err
	}

	messagesPerLabel := map[string][]MessageAttributes{}
	for _, file := range files {
		if !file.IsDir() {
			fileReader, err := os.Open(filepath.Join(root, file.Name()))
			if err != nil {
				return nil, errors.Wrap(err, "opening file")
			}

			if filepath.Ext(file.Name()) == ".eml" {
				label := filepath.Base(root)
				msg, err := readMessageAttributes(fileReader)
				if err != nil {
					return nil, err
				}
				messagesPerLabel[label] = append(messagesPerLabel[label], msg)
				sort.Sort(BySubject(messagesPerLabel[label]))
			} else if filepath.Ext(file.Name()) == ".mbox" {
				label := strings.TrimSuffix(file.Name(), ".mbox")
				mboxReader := mbox.NewReader(fileReader)
				for {
					msgReader, err := mboxReader.NextMessage()
					if err == io.EOF {
						break
					} else if err != nil {
						return nil, errors.Wrap(err, "reading next message")
					}
					msg, err := readMessageAttributes(msgReader)
					if err != nil {
						return nil, err
					}
					messagesPerLabel[label] = append(messagesPerLabel[label], msg)
				}
				sort.Sort(BySubject(messagesPerLabel[label]))
			}
		} else {
			subfolderRoot := filepath.Join(root, file.Name())
			subfolderMessagesPerLabel, err := readMessages(subfolderRoot)
			if err != nil {
				return nil, err
			}
			for key, value := range subfolderMessagesPerLabel {
				messagesPerLabel[key] = append(messagesPerLabel[key], value...)
				sort.Sort(BySubject(messagesPerLabel[key]))
			}
		}
	}
	return messagesPerLabel, nil
}

type MessageAttributes struct {
	subject string
	from    string
	to      []string
	date    int64
}

func readMessageAttributes(fileReader io.Reader) (MessageAttributes, error) {
	entity, err := message.Read(fileReader)
	if err != nil {
		return MessageAttributes{}, errors.Wrap(err, "reading file")
	}
	date, err := parseTime(entity.Header.Get("date"))
	if err != nil {
		return MessageAttributes{}, errors.Wrap(err, "parsing date")
	}
	from, err := parseAddress(entity.Header.Get("from"))
	if err != nil {
		return MessageAttributes{}, errors.Wrap(err, "parsing from")
	}
	to, err := parseAddresses(entity.Header.Get("to"))
	if err != nil {
		return MessageAttributes{}, errors.Wrap(err, "parsing to")
	}
	return MessageAttributes{
		subject: entity.Header.Get("subject"),
		from:    from,
		to:      to,
		date:    date.Unix(),
	}, nil
}

func parseTime(input string) (time.Time, error) {
	for _, format := range []string{time.RFC1123, time.RFC1123Z} {
		t, err := time.Parse(format, input)
		if err == nil {
			return t, nil
		}
	}
	return time.Time{}, errors.New("Unrecognized time format")
}

func parseAddresses(input string) ([]string, error) {
	addresses, err := mail.ParseAddressList(input)
	if err != nil {
		return nil, err
	}
	result := []string{}
	for _, address := range addresses {
		result = append(result, address.Address)
	}
	return result, nil
}

func parseAddress(input string) (string, error) {
	address, err := mail.ParseAddress(input)
	if err != nil {
		return "", err
	}
	return address.Address, nil
}

// BySubject implements sort.Interface based on the subject field.
type BySubject []MessageAttributes

func (a BySubject) Len() int           { return len(a) }
func (a BySubject) Less(i, j int) bool { return a[i].subject < a[j].subject }
func (a BySubject) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
