// 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 (
	"bytes"
	"fmt"
	"net/textproto"
	"os"
	"path/filepath"
	"strconv"
	"time"

	"github.com/ProtonMail/proton-bridge/pkg/message"
	"github.com/cucumber/godog"
	"github.com/cucumber/godog/gherkin"
	"github.com/emersion/go-imap"
	"github.com/emersion/go-mbox"
)

func TransferSetupFeatureContext(s *godog.Suite) {
	s.Step(`^there are EML files$`, thereAreEMLFiles)
	s.Step(`^there is EML file "([^"]*)"$`, thereIsEMLFile)
	s.Step(`^there is MBOX file "([^"]*)" with messages$`, thereIsMBOXFileWithMessages)
	s.Step(`^there is MBOX file "([^"]*)"$`, thereIsMBOXFile)
	s.Step(`^there are IMAP mailboxes$`, thereAreIMAPMailboxes)
	s.Step(`^there are IMAP messages$`, thereAreIMAPMessages)
	s.Step(`^there is IMAP message in mailbox "([^"]*)" with seq (\d+), uid (\d+), time "([^"]*)" and subject "([^"]*)"$`, thereIsIMAPMessage)
}

func thereAreEMLFiles(messages *gherkin.DataTable) error {
	head := messages.Rows[0].Cells
	for _, row := range messages.Rows[1:] {
		fileName := ""
		for n, cell := range row.Cells {
			switch head[n].Value {
			case "file":
				fileName = cell.Value
			case "from", "to", "subject", "time", "body":
			default:
				return fmt.Errorf("unexpected column name: %s", head[n].Value)
			}
		}

		body := getBodyFromDataRow(head, row)
		if err := createFile(fileName, body); err != nil {
			return err
		}
	}
	return nil
}

func thereIsEMLFile(fileName string, message *gherkin.DocString) error {
	return createFile(fileName, message.Content)
}

func thereIsMBOXFileWithMessages(fileName string, messages *gherkin.DataTable) error {
	mboxBuffer := &bytes.Buffer{}
	mboxWriter := mbox.NewWriter(mboxBuffer)

	head := messages.Rows[0].Cells
	for _, row := range messages.Rows[1:] {
		from := ""
		for n, cell := range row.Cells {
			switch head[n].Value {
			case "from":
				from = cell.Value
			case "to", "subject", "time", "body":
			default:
				return fmt.Errorf("unexpected column name: %s", head[n].Value)
			}
		}

		body := getBodyFromDataRow(head, row)

		messageWriter, err := mboxWriter.CreateMessage(from, time.Now())
		if err != nil {
			return err
		}
		_, err = messageWriter.Write([]byte(body))
		if err != nil {
			return err
		}
	}

	return createFile(fileName, mboxBuffer.String())
}

func thereIsMBOXFile(fileName string, messages *gherkin.DocString) error {
	return createFile(fileName, messages.Content)
}

func thereAreIMAPMailboxes(mailboxes *gherkin.DataTable) error {
	imapServer := ctx.GetTransferRemoteIMAPServer()
	head := mailboxes.Rows[0].Cells
	for _, row := range mailboxes.Rows[1:] {
		mailboxName := ""
		for n, cell := range row.Cells {
			switch head[n].Value {
			case "name":
				mailboxName = cell.Value
			default:
				return fmt.Errorf("unexpected column name: %s", head[n].Value)
			}
		}
		imapServer.AddMailbox(mailboxName)
	}
	return nil
}

func thereAreIMAPMessages(messages *gherkin.DataTable) (err error) {
	imapServer := ctx.GetTransferRemoteIMAPServer()
	head := messages.Rows[0].Cells
	for _, row := range messages.Rows[1:] {
		mailboxName := ""
		date := time.Now()
		subject := ""
		seqNum := 0
		uid := 0
		for n, cell := range row.Cells {
			switch head[n].Value {
			case "mailbox":
				mailboxName = cell.Value
			case "uid":
				uid, err = strconv.Atoi(cell.Value)
				if err != nil {
					return internalError(err, "failed to parse uid")
				}
			case "seqnum":
				seqNum, err = strconv.Atoi(cell.Value)
				if err != nil {
					return internalError(err, "failed to parse seqnum")
				}
			case "time":
				date, err = time.Parse(timeFormat, cell.Value)
				if err != nil {
					return internalError(err, "failed to parse time")
				}
			case "subject":
				subject = cell.Value
			case "from", "to", "body":
			default:
				return fmt.Errorf("unexpected column name: %s", head[n].Value)
			}
		}

		body := getBodyFromDataRow(head, row)
		imapMessage, err := getIMAPMessage(seqNum, uid, date, subject, body)
		if err != nil {
			return err
		}
		imapServer.AddMessage(mailboxName, imapMessage)
	}
	return nil
}

func thereIsIMAPMessage(mailboxName string, seqNum, uid int, dateValue, subject string, message *gherkin.DocString) error {
	imapServer := ctx.GetTransferRemoteIMAPServer()

	date, err := time.Parse(timeFormat, dateValue)
	if err != nil {
		return internalError(err, "failed to parse time")
	}

	imapMessage, err := getIMAPMessage(seqNum, uid, date, subject, message.Content)
	if err != nil {
		return err
	}
	imapServer.AddMessage(mailboxName, imapMessage)

	return nil
}

func getBodyFromDataRow(head []*gherkin.TableCell, row *gherkin.TableRow) string {
	body := "hello"
	headers := textproto.MIMEHeader{}
	for n, cell := range row.Cells {
		switch head[n].Value {
		case "from":
			headers.Set("from", cell.Value)
		case "to":
			headers.Set("to", cell.Value)
		case "subject":
			headers.Set("subject", cell.Value)
		case "time":
			date, err := time.Parse(timeFormat, cell.Value)
			if err != nil {
				panic(err)
			}
			headers.Set("date", date.Format(time.RFC1123))
		case "body":
			body = cell.Value
		}
	}

	buffer := &bytes.Buffer{}
	_ = message.WriteHeader(buffer, headers)
	return buffer.String() + body + "\n\n"
}

func getIMAPMessage(seqNum, uid int, date time.Time, subject, body string) (*imap.Message, error) {
	reader := bytes.NewBufferString(body)
	bodyStructure, err := message.NewBodyStructure(reader)
	if err != nil {
		return nil, internalError(err, "failed to parse body structure")
	}
	imapBodyStructure, err := bodyStructure.IMAPBodyStructure([]int{})
	if err != nil {
		return nil, internalError(err, "failed to parse body structure")
	}
	bodySection, _ := imap.ParseBodySectionName("BODY[]")

	return &imap.Message{
		SeqNum: uint32(seqNum),
		Uid:    uint32(uid),
		Size:   uint32(len(body)),
		Envelope: &imap.Envelope{
			Date:    date,
			Subject: subject,
		},
		BodyStructure: imapBodyStructure,
		Body: map[*imap.BodySectionName]imap.Literal{
			bodySection: bytes.NewBufferString(body),
		},
	}, nil
}

func createFile(fileName, body string) error {
	root := ctx.GetTransferLocalRootForImport()
	filePath := filepath.Join(root, fileName)

	dirPath := filepath.Dir(filePath)
	err := os.MkdirAll(dirPath, os.ModePerm)
	if err != nil {
		return internalError(err, "failed to create dir")
	}

	f, err := os.Create(filePath)
	if err != nil {
		return internalError(err, "failed to create file")
	}
	defer f.Close() //nolint

	_, err = f.WriteString(body)
	return internalError(err, "failed to write to file")
}
