// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package tests contains end to end tests meant to verify the Cloud SQL Auth proxy
// works as expected when executed as a binary.
//
// Required flags:
//
//	-mysql_conn_name, -db_user, -db_pass
package tests

import (
	"bufio"
	"bytes"
	"context"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path"
	"runtime"
	"strings"
	"testing"
)

var (
	binPath = ""
)

func TestMain(m *testing.M) {
	flag.Parse()
	// compile the proxy as a binary
	var err error
	binPath, err = compileProxy()
	if err != nil {
		log.Fatalf("failed to compile proxy: %s", err)
	}
	// Run tests and cleanup
	rtn := m.Run()
	os.RemoveAll(binPath)

	os.Exit(rtn)
}

// compileProxy compiles the binary into a temporary directory, and returns the path to the file or any error that occured.
func compileProxy() (string, error) {
	// get path of the cmd pkg
	_, f, _, ok := runtime.Caller(0)
	if !ok {
		return "", fmt.Errorf("failed to find cmd pkg")
	}
	projRoot := path.Dir(path.Dir(f)) // cd ../..
	pkgPath := path.Join(projRoot, "cmd", "cloud_sql_proxy")
	// compile the proxy into a tmp directory
	tmp, err := ioutil.TempDir("", "")
	if err != nil {
		return "", fmt.Errorf("failed to create temp dir: %s", err)
	}

	b := path.Join(tmp, "cloud_sql_proxy")

	if runtime.GOOS == "windows" {
		b += ".exe"
	}

	cmd := exec.Command("go", "build", "-o", b, pkgPath)
	out, err := cmd.CombinedOutput()
	if err != nil {
		return "", fmt.Errorf("failed to run 'go build': %w \n %s", err, out)
	}
	return b, nil
}

// proxyExec represents an execution of the Cloud SQL proxy.
type ProxyExec struct {
	Out io.ReadCloser

	cmd     *exec.Cmd
	cancel  context.CancelFunc
	closers []io.Closer
	done    chan bool // closed once the cmd is completed
	err     error
}

// StartProxy returns a proxyExec representing a running instance of the proxy.
func StartProxy(ctx context.Context, args ...string) (*ProxyExec, error) {
	var err error
	ctx, cancel := context.WithCancel(ctx)
	p := ProxyExec{
		cmd:    exec.CommandContext(ctx, binPath, args...),
		cancel: cancel,
		done:   make(chan bool),
	}
	pr, pw, err := os.Pipe()
	if err != nil {
		return nil, fmt.Errorf("unable to open stdout pipe: %w", err)
	}
	defer pw.Close()
	p.Out, p.cmd.Stdout, p.cmd.Stderr = pr, pw, pw
	p.closers = append(p.closers, pr)
	if err := p.cmd.Start(); err != nil {
		defer p.Close()
		return nil, fmt.Errorf("unable to start cmd: %w", err)
	}
	// when process is complete, mark as finished
	go func() {
		defer close(p.done)
		p.err = p.cmd.Wait()
	}()
	return &p, nil
}

// Stop sends the pskill signal to the proxy and returns.
func (p *ProxyExec) Kill() {
	p.cancel()
}

// Waits until the execution is completed and returns any error.
func (p *ProxyExec) Wait() error {
	select {
	case <-p.done:
		return p.err
	}
}

// Stop sends the pskill signal to the proxy and returns.
func (p *ProxyExec) Done() bool {
	select {
	case <-p.done:
		return true
	default:
	}
	return false
}

// Close releases any resources assotiated with the instance.
func (p *ProxyExec) Close() {
	p.cancel()
	for _, c := range p.closers {
		c.Close()
	}
}

// WaitForServe waits until the proxy ready to serve traffic. Returns any output from the proxy
// while starting or any errors experienced before the proxy was ready to server.
func (p *ProxyExec) WaitForServe(ctx context.Context) (output string, err error) {
	// Watch for the "Ready for new connections" to indicate the proxy is listening
	buf, in, errCh := new(bytes.Buffer), bufio.NewReader(p.Out), make(chan error, 1)
	go func() {
		defer close(errCh)
		for {
			// if ctx is finished, stop processing
			select {
			case <-ctx.Done():
				return
			default:
			}
			s, err := in.ReadString('\n')
			if err != nil {
				errCh <- err
				return
			}
			buf.WriteString(s)
			if strings.Contains(s, "Ready for new connections") {
				errCh <- nil
				return
			}
		}
	}()
	// Wait for either the background thread of the context to complete
	select {
	case <-ctx.Done():
		return buf.String(), fmt.Errorf("context done: %w", ctx.Err())
	case err := <-errCh:
		if err != nil {
			return buf.String(), fmt.Errorf("proxy start failed: %w", err)
		}
	}
	return buf.String(), nil
}
