// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge.  If not, see <https://www.gnu.org/licenses/>.

package tests

import (
	"bytes"
	"fmt"
	"io"
	"reflect"
	"strconv"
	"strings"
	"time"

	"github.com/ProtonMail/gluon/rfc822"
	"github.com/ProtonMail/proton-bridge/v3/pkg/message"
	"github.com/bradenaw/juniper/xslices"
	"github.com/cucumber/messages-go/v16"
	"github.com/emersion/go-imap"
	"golang.org/x/exp/slices"
)

type Message struct {
	Subject     string `bdd:"subject"`
	Body        string `bdd:"body"`
	MIMEType    string `bdd:"mime-type"`
	Attachments string `bdd:"attachments"`
	MessageID   string `bdd:"message-id"`
	Date        string `bdd:"date"`

	From string `bdd:"from"`
	To   string `bdd:"to"`
	CC   string `bdd:"cc"`
	BCC  string `bdd:"bcc"`

	Unread  bool `bdd:"unread"`
	Deleted bool `bdd:"deleted"`
}

func (msg Message) Build() []byte {
	var b []byte

	if msg.From != "" {
		b = append(b, "From: "+msg.From+"\r\n"...)
	}

	if msg.To != "" {
		b = append(b, "To: "+msg.To+"\r\n"...)
	}

	if msg.CC != "" {
		b = append(b, "Cc: "+msg.CC+"\r\n"...)
	}

	if msg.BCC != "" {
		b = append(b, "Bcc: "+msg.BCC+"\r\n"...)
	}

	if msg.Subject != "" {
		b = append(b, "Subject: "+msg.Subject+"\r\n"...)
	}

	if msg.Date != "" {
		date, err := time.Parse(time.RFC822, msg.Date)
		if err != nil {
			panic(err)
		}
		b = append(b, "Date: "+date.Format(time.RFC822Z)+"\r\n"...)
	}

	b = append(b, "\r\n"+msg.Body+"\r\n"...)

	return b
}

func newMessageFromIMAP(msg *imap.Message) Message {
	section, err := imap.ParseBodySectionName("BODY[]")
	if err != nil {
		panic(err)
	}

	literal, err := io.ReadAll(msg.GetBody(section))
	if err != nil {
		panic(err)
	}

	mimeType, _, err := rfc822.Parse(literal).ContentType()
	if err != nil {
		panic(err)
	}

	m, err := message.Parse(bytes.NewReader(literal))
	if err != nil {
		panic(err)
	}

	var body string

	if m.MIMEType == rfc822.TextPlain {
		body = strings.TrimSpace(string(m.PlainBody))
	} else {
		body = strings.TrimSpace(string(m.RichBody))
	}

	message := Message{
		Subject:     msg.Envelope.Subject,
		Body:        body,
		MIMEType:    string(mimeType),
		Attachments: strings.Join(xslices.Map(m.Attachments, func(att message.Attachment) string { return att.Name }), ", "),
		MessageID:   msg.Envelope.MessageId,
		Unread:      !slices.Contains(msg.Flags, imap.SeenFlag),
		Deleted:     slices.Contains(msg.Flags, imap.DeletedFlag),
		Date:        msg.Envelope.Date.Format(time.RFC822Z),
	}

	if len(msg.Envelope.From) > 0 {
		message.From = msg.Envelope.From[0].Address()
	}

	if len(msg.Envelope.To) > 0 {
		message.To = msg.Envelope.To[0].Address()
	}

	if len(msg.Envelope.Cc) > 0 {
		message.CC = msg.Envelope.Cc[0].Address()
	}

	if len(msg.Envelope.Bcc) > 0 {
		message.BCC = msg.Envelope.Bcc[0].Address()
	}

	return message
}

func matchMessages(have, want []Message) error {
	slices.SortFunc(have, func(a, b Message) bool {
		return a.Subject < b.Subject
	})

	slices.SortFunc(want, func(a, b Message) bool {
		return a.Subject < b.Subject
	})

	if !IsSub(ToAny(have), ToAny(want)) {
		return fmt.Errorf("missing messages: have %#v, want %#v", have, want)
	}

	return nil
}

type Mailbox struct {
	Name   string `bdd:"name"`
	Total  int    `bdd:"total"`
	Unread int    `bdd:"unread"`
}

func newMailboxFromIMAP(status *imap.MailboxStatus) Mailbox {
	return Mailbox{
		Name:   status.Name,
		Total:  int(status.Messages),
		Unread: int(status.Unseen),
	}
}

func matchMailboxes(have, want []Mailbox) error {
	slices.SortFunc(have, func(a, b Mailbox) bool {
		return a.Name < b.Name
	})

	slices.SortFunc(want, func(a, b Mailbox) bool {
		return a.Name < b.Name
	})

	if !IsSub(want, have) {
		return fmt.Errorf("missing mailboxes: %v", want)
	}

	return nil
}

func eventually(condition func() error) error {
	ch := make(chan error, 1)

	timer := time.NewTimer(30 * time.Second)
	defer timer.Stop()

	ticker := time.NewTicker(100 * time.Millisecond)
	defer ticker.Stop()

	for tick := ticker.C; ; {
		select {
		case <-timer.C:
			return fmt.Errorf("timed out")

		case <-tick:
			tick = nil

			go func() { ch <- condition() }()

		case err := <-ch:
			if err == nil {
				return nil
			}

			tick = ticker.C
		}
	}
}

func unmarshalTable[T any](table *messages.PickleTable) ([]T, error) {
	if len(table.Rows) == 0 {
		return nil, fmt.Errorf("empty table")
	}

	res := make([]T, 0, len(table.Rows))

	for _, row := range table.Rows[1:] {
		var v T

		if err := unmarshalRow(table.Rows[0], row, &v); err != nil {
			return nil, err
		}

		res = append(res, v)
	}

	return res, nil
}

func unmarshalRow(header, row *messages.PickleTableRow, v any) error {
	typ := reflect.TypeOf(v).Elem()

	for idx := 0; idx < typ.NumField(); idx++ {
		field := typ.Field(idx)

		if tag, ok := field.Tag.Lookup("bdd"); ok {
			cell, ok := getCellValue(header, row, tag)
			if !ok {
				continue
			}

			switch field.Type.Kind() { //nolint:exhaustive
			case reflect.String:
				reflect.ValueOf(v).Elem().Field(idx).SetString(cell)

			case reflect.Int:
				reflect.ValueOf(v).Elem().Field(idx).SetInt(int64(mustParseInt(cell)))

			case reflect.Bool:
				reflect.ValueOf(v).Elem().Field(idx).SetBool(mustParseBool(cell))

			default:
				return fmt.Errorf("unsupported type %q", field.Type.Kind())
			}
		}
	}

	return nil
}

func getCellValue(header, row *messages.PickleTableRow, name string) (string, bool) {
	for idx, cell := range header.Cells {
		if cell.Value == name {
			return row.Cells[idx].Value, true
		}
	}

	return "", false
}

func mustParseInt(s string) int {
	i, err := strconv.Atoi(s)
	if err != nil {
		panic(err)
	}

	return i
}

func mustParseBool(s string) bool {
	v, err := strconv.ParseBool(s)
	if err != nil {
		panic(err)
	}

	return v
}
