// Copyright 2021 Molecula Corp. All rights reserved.
package pg_test

import (
	"bytes"
	"context"
	"crypto/rand"
	"fmt"
	"net"
	"os/exec"
	"strconv"
	"sync"
	"syscall"
	"testing"
	"time"

	"github.com/lib/pq"
	"github.com/molecula/featurebase/v3/logger"
	"github.com/molecula/featurebase/v3/pg"
	"github.com/molecula/featurebase/v3/pg/pgtest"
)

// TestStartupTimeout tests that an incoming connection that does nothing times out and gets closed.
func TestStartupTimeout(t *testing.T) {
	t.Parallel()

	connect, shutdown, err := pgtest.ServeMem(&pg.Server{
		StartupTimeout: time.Millisecond,
		Logger:         logger.NopLogger,
	})
	if err != nil {
		t.Fatalf("starting in-memory postgres server: %v", err)
	}
	defer shutdown.Finish(t, "in-memory postgres server")

	conn, err := connect()
	if err != nil {
		t.Fatalf("failed to acquire connection: %v", err)
	}
	defer conn.Close()

	// The server isn't sending anything, so this should block until the connection dies.
	conn.Read(make([]byte, 1024)) //nolint:errcheck
}

// TestStartupInvalidLength tests that sending an HTTP GET request does not cause the server to allocate 1.2 GiB of memory.
func TestStartupInvalidLength(t *testing.T) {
	t.Parallel()

	res := testing.Benchmark(func(b *testing.B) {
		connect, shutdown, err := pgtest.ServeMem(&pg.Server{
			MaxStartupSize: 1024,
			Logger:         logger.NopLogger,
		})
		if err != nil {
			t.Fatalf("starting in-memory postgres server: %v", err)
		}
		defer shutdown.Finish(t, "in-memory postgres server")

		b.ReportAllocs()

		b.ResetTimer()

		for i := 0; i < b.N; i++ {
			conn, err := connect()
			if err != nil {
				t.Fatalf("failed to acquire connection: %v", err)
			}

			_, err = conn.Write([]byte("GET "))
			if err != nil {
				t.Fatalf("failed to write invalid length: %v", err)
			}

			// The server isn't sending anything, so this should block until the connection dies.
			conn.Read(make([]byte, 1024)) //nolint:errcheck

			err = conn.Close()
			if err != nil {
				t.Fatalf("failed to close connection: %v", err)
			}
		}
	})
	bpo := res.AllocedBytesPerOp()
	t.Logf("allocated %d bytes per op", bpo)
	if bpo > 1024*1024 {
		t.Errorf("allocated too much memory: %d bytes/connection", bpo)
	}
}

// TestPQConnect tests connecting the Go SQL driver `pq` to this postgres server.
func TestPQConnect(t *testing.T) {
	t.Parallel()

	server := &pg.Server{
		StartupTimeout: time.Second,
		Logger:         logger.NopLogger,
	}

	addr, shutdown, err := pgtest.ServeTCP(":0", server)
	if err != nil {
		t.Fatalf("starting postgres server: %v", err)
	}
	defer shutdown.Finish(t, "postgres server")

	tcpAddr := addr.(*net.TCPAddr)

	connector, err := pq.NewConnector(fmt.Sprintf("user=molecula dbname=pilosa sslmode=disable host=%s port=%d", tcpAddr.IP, tcpAddr.Port))
	if err != nil {
		t.Fatalf("failed to create connector: %v", err)
	}

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

	conn, err := connector.Connect(ctx)
	if err != nil {
		t.Fatalf("failed to connect to postgres: %v", err)
	}
	defer pgtest.ShutdownFunc(conn.Close).Finish(t, "postgres TLS conn")
}

// TestPQConnectSSL tests connecting the Go SQL driver `pq` to this postgres server, with SSL enabled.
func TestPQConnectSSL(t *testing.T) {
	t.Parallel()

	server := &pg.Server{
		StartupTimeout: time.Second,
		Logger:         logger.NopLogger,
	}

	addr, shutdown, err := pgtest.ServeTLS(":0", server)
	if err != nil {
		t.Fatalf("starting postgres server: %v", err)
	}
	defer shutdown.Finish(t, "postgres TLS server")

	tcpAddr := addr.(*net.TCPAddr)

	connector, err := pq.NewConnector(fmt.Sprintf("user=molecula dbname=pilosa sslmode=require host=%s port=%d", tcpAddr.IP, tcpAddr.Port))
	if err != nil {
		t.Fatalf("failed to create connector: %v", err)
	}

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

	conn, err := connector.Connect(ctx)
	if err != nil {
		t.Fatalf("failed to connect to postgres: %v", err)
	}
	defer pgtest.ShutdownFunc(conn.Close).Finish(t, "postgres TLS conn")
}

// TestPSQLQuery tests sending a query from the `psql` command line tool.
func TestPSQLQuery(t *testing.T) {
	// Check if psql is present.
	// Skip this test if it is not.
	_, err := exec.LookPath("psql")
	if err != nil {
		if err, ok := err.(*exec.Error); ok {
			if err.Err == exec.ErrNotFound {
				t.Skip("psql is not available")
			}
		}
		t.Fatalf("searching for psql: %v", err)
	}

	t.Run("Query", func(t *testing.T) {
		server := &pg.Server{
			QueryHandler: pgtest.HandlerFunc(func(ctx context.Context, w pg.QueryResultWriter, q pg.Query) error {
				err := w.WriteHeader(pg.ColumnInfo{
					Name: "field",
					Type: pg.TypeCharoid,
				})
				if err != nil {
					return err
				}

				err = w.WriteRowText("h")
				if err != nil {
					return err
				}

				err = w.WriteRowText("xyzzy")
				if err != nil {
					return err
				}

				return nil
			}),
			TypeEngine:     pg.PrimitiveTypeEngine{},
			StartupTimeout: time.Second,
			Logger:         logger.NopLogger,
		}

		addr, shutdown, err := pgtest.ServeTCP(":0", server)
		if err != nil {
			t.Fatalf("starting postgres server: %v", err)
		}
		defer shutdown.Finish(t, "postgres server")

		tcpAddr := addr.(*net.TCPAddr)

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

		cmd := exec.CommandContext(ctx, "psql", "-h", tcpAddr.IP.String(), "-p", strconv.Itoa(tcpAddr.Port), "-c", "test query")
		data, err := cmd.CombinedOutput()
		if err != nil {
			t.Fatalf("psql failed: %v", string(data))
		}
	})

	t.Run("Cancel", func(t *testing.T) {
		var term func() error
		var qerr error
		var qdone bool
		defer func() {
			if qerr != nil {
				t.Fatal(qerr)
			}
			if !qdone {
				t.Fatal("query not done")
			}
		}()

		var mutex sync.Mutex
		mutex.Lock()

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

		server := &pg.Server{
			QueryHandler: pgtest.HandlerFunc(func(qctx context.Context, w pg.QueryResultWriter, q pg.Query) error {
				defer func() { qdone = true }()

				mutex.Lock()
				defer mutex.Unlock()

				qerr = term()
				if qerr != nil {
					return qerr
				}
				select {
				case <-qctx.Done():
				case <-ctx.Done():
					qerr = ctx.Err()
					return qerr
				}
				return nil
			}),
			TypeEngine:          pg.PrimitiveTypeEngine{},
			StartupTimeout:      time.Second,
			Logger:              logger.NopLogger,
			CancellationManager: pg.NewLocalCancellationManager(rand.Reader),
		}

		addr, shutdown, err := pgtest.ServeTCP(":0", server)
		if err != nil {
			t.Fatalf("starting postgres server: %v", err)
		}
		defer shutdown.Finish(t, "postgres server")

		tcpAddr := addr.(*net.TCPAddr)

		cmd := exec.CommandContext(ctx, "psql", "-h", tcpAddr.IP.String(), "-p", strconv.Itoa(tcpAddr.Port), "-c", "test query")
		term = func() error { return cmd.Process.Signal(syscall.SIGINT) }
		var buf bytes.Buffer
		cmd.Stderr = &buf
		cmd.Stdout = &buf
		err = cmd.Start()
		if err != nil {
			t.Fatalf("starting postgres client: %v", err)
		}
		mutex.Unlock()
		err = cmd.Wait()
		if err != nil {
			t.Fatalf("psql failed (%v): %s", err, buf.String())
		}
	})
}
