package telegram_test

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/hex"
	"io"
	"os"
	"os/exec"
	"strings"
	"testing"
	"time"

	"github.com/go-faster/errors"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap/zaptest"

	"go.mau.fi/mautrix-telegram/pkg/gotd/session"
	"go.mau.fi/mautrix-telegram/pkg/gotd/tdsync"
	"go.mau.fi/mautrix-telegram/pkg/gotd/telegram"
	"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/dcs"
)

type mtg struct {
	path string
	addr string
}

type signalWriter struct {
	io.Writer
	wait *tdsync.Ready
}

func (s signalWriter) Write(p []byte) (n int, err error) {
	s.wait.Signal()
	return s.Writer.Write(p)
}

func (m mtg) run(ctx context.Context, secret string, out, err io.Writer, wait *tdsync.Ready) error {
	cmd := exec.CommandContext(ctx, m.path, "simple-run", "-d", m.addr, secret)
	cmd.Stdout = signalWriter{Writer: out, wait: wait}
	cmd.Stderr = signalWriter{Writer: err, wait: wait}
	cmd.Env = append([]string{"MTG_DEBUG=true", "MTG_TEST_DC=true"}, os.Environ()...)
	return cmd.Run()
}

func (m mtg) generateSecret(ctx context.Context, _ string) ([]byte, error) {
	args := []string{"generate-secret", "google.com"}

	o, err := exec.CommandContext(ctx, m.path, args...).Output()
	if err != nil {
		return nil, errors.Wrap(err, "execute command")
	}
	output := strings.TrimSpace(string(o))

	r, err := base64.RawURLEncoding.DecodeString(output)
	if err != nil {
		return nil, errors.Wrapf(err, "decode secret %q", output)
	}

	return r, nil
}

func testMTProxy(secretType string, m mtg, storage session.Storage) func(t *testing.T) {
	return func(t *testing.T) {
		a := require.New(t)
		logger := zaptest.NewLogger(t)

		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
		defer cancel()

		secret, err := m.generateSecret(ctx, secretType)
		a.NoError(err)

		// Store mtg logs to buffer and print it only if test failed.
		w := &bytes.Buffer{}
		t.Cleanup(func() {
			if t.Failed() {
				_, _ = io.Copy(os.Stdout, w)
			}
		})

		g := tdsync.NewCancellableGroup(ctx)
		ready := tdsync.NewReady()
		g.Go(func(ctx context.Context) error {
			err := m.run(ctx, hex.EncodeToString(secret), w, w, ready)
			select {
			case <-ctx.Done():
				return nil
			default:
				return err
			}
		})
		g.Go(func(ctx context.Context) error {
			defer g.Cancel()
			select {
			case <-ready.Ready():
			case <-ctx.Done():
				return ctx.Err()
			}

			resolver, err := dcs.MTProxy(m.addr, secret, dcs.MTProxyOptions{})
			if err != nil {
				return err
			}

			return tryConnect(ctx, telegram.Options{
				Resolver:       resolver,
				Logger:         logger,
				SessionStorage: storage,
				DCList:         dcs.Prod(),
			})
		})

		a.NoError(g.Wait())
	}
}

func TestExternalE2EMTProxy(t *testing.T) {
	addr, ok := os.LookupEnv("GOTD_MTPROXY_ADDR")
	if !ok {
		t.Skip("Skipped. Set GOTD_MTPROXY_ADDR to enable external e2e mtproxy test.")
	}

	mtgPath, err := exec.LookPath("mtg")
	if err != nil {
		t.Fatal("mtg binary not found", err)
	}

	// To re-use session.
	storage := &session.StorageMemory{}
	m := mtg{path: mtgPath, addr: addr}
	// TODO(tdakkota): test all proxy types (mtg v2 supports only faketls)
	for _, secretType := range []string{"tls"} {
		t.Run(strings.Title(secretType), testMTProxy(secretType, m, storage))
	}
}
