// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package tlsdial

import (
	"crypto/x509"
	"io"
	"net"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"reflect"
	"runtime"
	"sync/atomic"
	"testing"

	"tailscale.com/health"
)

func resetOnce() {
	rv := reflect.ValueOf(&bakedInRootsOnce).Elem()
	rv.Set(reflect.Zero(rv.Type()))
}

func TestBakedInRoots(t *testing.T) {
	resetOnce()
	p := bakedInRoots()
	got := p.Subjects()
	if len(got) != 1 {
		t.Errorf("subjects = %v; want 1", len(got))
	}
}

func TestFallbackRootWorks(t *testing.T) {
	defer resetOnce()

	const debug = false
	if runtime.GOOS != "linux" {
		t.Skip("test assumes Linux")
	}
	d := t.TempDir()
	crtFile := filepath.Join(d, "tlsdial.test.crt")
	keyFile := filepath.Join(d, "tlsdial.test.key")
	caFile := filepath.Join(d, "rootCA.pem")
	cmd := exec.Command("go",
		"run", "filippo.io/mkcert",
		"--cert-file="+crtFile,
		"--key-file="+keyFile,
		"tlsdial.test")
	cmd.Env = append(os.Environ(), "CAROOT="+d)
	out, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("mkcert: %v, %s", err, out)
	}
	if debug {
		t.Logf("Ran: %s", out)
		dents, err := os.ReadDir(d)
		if err != nil {
			t.Fatal(err)
		}
		for _, de := range dents {
			t.Logf(" - %v", de)
		}
	}

	caPEM, err := os.ReadFile(caFile)
	if err != nil {
		t.Fatal(err)
	}
	resetOnce()
	bakedInRootsOnce.Do(func() {
		p := x509.NewCertPool()
		if !p.AppendCertsFromPEM(caPEM) {
			t.Fatal("failed to add")
		}
		bakedInRootsOnce.p = p
	})

	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatal(err)
	}
	defer ln.Close()
	if debug {
		t.Logf("listener running at %v", ln.Addr())
	}
	done := make(chan struct{})
	defer close(done)

	errc := make(chan error, 1)
	go func() {
		err := http.ServeTLS(ln, http.HandlerFunc(sayHi), crtFile, keyFile)
		select {
		case <-done:
			return
		default:
			t.Logf("ServeTLS: %v", err)
			errc <- err
		}
	}()

	tr := &http.Transport{
		Dial: func(network, addr string) (net.Conn, error) {
			return net.Dial("tcp", ln.Addr().String())
		},
		DisableKeepAlives: true, // for test cleanup ease
	}
	ht := new(health.Tracker)
	tr.TLSClientConfig = Config("tlsdial.test", ht, tr.TLSClientConfig)
	c := &http.Client{Transport: tr}

	ctr0 := atomic.LoadInt32(&counterFallbackOK)

	res, err := c.Get("https://tlsdial.test/")
	if err != nil {
		t.Fatal(err)
	}
	defer res.Body.Close()
	if res.StatusCode != 200 {
		t.Fatal(res.Status)
	}

	ctrDelta := atomic.LoadInt32(&counterFallbackOK) - ctr0
	if ctrDelta != 1 {
		t.Errorf("fallback root success count = %d; want 1", ctrDelta)
	}
}

func sayHi(w http.ResponseWriter, r *http.Request) {
	io.WriteString(w, "hi")
}
