//go:build integration

package integration

import (
	"bufio"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"net/http"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/eggsampler/acme/v3"
)

func streamOutput(t *testing.T, c *exec.Cmd) (<-chan string, func()) {
	t.Helper()
	outChan := make(chan string)

	stdout, err := c.StdoutPipe()
	if err != nil {
		t.Fatalf("getting stdout handle: %s", err)
	}

	outScanner := bufio.NewScanner(stdout)
	go func() {
		for outScanner.Scan() {
			outChan <- outScanner.Text()
		}
	}()

	stderr, err := c.StderrPipe()
	if err != nil {
		t.Fatalf("getting stderr handle: %s", err)
	}

	errScanner := bufio.NewScanner(stderr)
	go func() {
		for errScanner.Scan() {
			outChan <- errScanner.Text()
		}
	}()

	err = c.Start()
	if err != nil {
		t.Fatalf("starting cmd: %s", err)
	}

	return outChan, func() {
		c.Cancel()
		c.Wait()
	}
}

func TestTLSProbe(t *testing.T) {
	t.Parallel()

	// We can't use random_domain(), because the observer needs to be able to
	// resolve this hostname within the docker-compose environment.
	hostname := "integration.trust"
	tempdir := t.TempDir()

	// Create the certificate that the prober will inspect.
	client, err := makeClient()
	if err != nil {
		t.Fatalf("creating test acme client: %s", err)
	}

	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		t.Fatalf("generating test key: %s", err)
	}

	res, err := authAndIssue(client, key, []acme.Identifier{{Type: "dns", Value: hostname}}, true, "")
	if err != nil {
		t.Fatalf("issuing test cert: %s", err)
	}

	// Set up the HTTP server that the prober will be pointed at.
	certFile, err := os.Create(path.Join(tempdir, "fullchain.pem"))
	if err != nil {
		t.Fatalf("creating cert file: %s", err)
	}

	err = pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: res.certs[0].Raw})
	if err != nil {
		t.Fatalf("writing test cert to file: %s", err)
	}

	err = pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: res.certs[1].Raw})
	if err != nil {
		t.Fatalf("writing test issuer cert to file: %s", err)
	}

	err = certFile.Close()
	if err != nil {
		t.Errorf("closing cert file: %s", err)
	}

	keyFile, err := os.Create(path.Join(tempdir, "privkey.pem"))
	if err != nil {
		t.Fatalf("creating key file: %s", err)
	}

	keyDER, err := x509.MarshalECPrivateKey(key)
	if err != nil {
		t.Fatalf("marshalling test key: %s", err)
	}

	err = pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
	if err != nil {
		t.Fatalf("writing test key to file: %s", err)
	}

	err = keyFile.Close()
	if err != nil {
		t.Errorf("closing key file: %s", err)
	}

	go http.ListenAndServeTLS(":8675", certFile.Name(), keyFile.Name(), http.DefaultServeMux)

	// Kick off the prober, pointed at the server presenting our test cert.
	configFile, err := os.Create(path.Join(tempdir, "observer.yml"))
	if err != nil {
		t.Fatalf("creating config file: %s", err)
	}

	_, err = configFile.WriteString(fmt.Sprintf(`---
buckets: [.001, .002, .005, .01, .02, .05, .1, .2, .5, 1, 2, 5, 10]
syslog:
  stdoutlevel: 6
  sysloglevel: 0
monitors:
  -
    period: 1s
    kind: TLS
    settings:
      response: valid
      hostname: "%s:8675"`, hostname))
	if err != nil {
		t.Fatalf("writing test config: %s", err)
	}

	binPath, err := filepath.Abs("bin/boulder")
	if err != nil {
		t.Fatalf("computing boulder binary path: %s", err)
	}

	c := exec.CommandContext(t.Context(), binPath, "boulder-observer", "-config", configFile.Name(), "-debug-addr", ":8024")
	output, cancel := streamOutput(t, c)
	defer cancel()

	timeout := time.NewTimer(5 * time.Second)

	for {
		select {
		case <-timeout.C:
			t.Fatalf("timed out before getting desired log line from boulder-observer")
		case line := <-output:
			t.Log(line)
			if strings.Contains(line, "name=[integration.trust:8675]") && strings.Contains(line, "success=[true]") {
				return
			}
		}
	}
}
