package acmeserver

import (
	"context"
	"crypto"
	"crypto/x509"
	"errors"
	"io/ioutil"
	"os"
	"path/filepath"
	"testing"
	"time"
)

// A CertGenerator that is just very slow (and will return an error
// in any case).
type slowACME struct{}

func (s *slowACME) GetCertificate(ctx context.Context, _ crypto.Signer, _ *certConfig) ([][]byte, *x509.Certificate, error) {
	t := time.After(60 * time.Second)
	select {
	case <-t:
		return nil, nil, errors.New("timed out")
	case <-ctx.Done():
		return nil, nil, ctx.Err()
	}
}

func newTestConfig(dir string) *Config {
	c := Config{
		Dirs:           []string{filepath.Join(dir, "config")},
		AccountKeyPath: filepath.Join(dir, "account.key"),
		Email:          "test@example.com",
	}
	c.Output.Path = filepath.Join(dir, "certs")
	return &c
}

// Create a new test function.
// The first function returned is a cleanup callback.
// The second function returned is the cancel callback.
func newTestManager(t testing.TB, g CertGenerator) (func(), context.CancelFunc, *Manager) {
	dir, err := ioutil.TempDir("", "")
	if err != nil {
		t.Fatal(err)
	}

	os.Mkdir(filepath.Join(dir, "config"), 0700)
	ioutil.WriteFile(
		filepath.Join(dir, "config", "test.yml"),
		[]byte("- { names: [example.com] }\n"),
		0644,
	)

	m, err := NewManager(newTestConfig(dir), g)
	if err != nil {
		t.Fatal(err)
	}

	ctx, cancel := context.WithCancel(context.Background())
	if err := m.Start(ctx); err != nil {
		t.Fatal("Start:", err)
	}

	// Wait just a little bit to give a chance to m.loop() to run.
	time.Sleep(50 * time.Millisecond)

	return func() {
		cancel()
		m.Wait()
		os.RemoveAll(dir)
	}, cancel, m
}

func TestManager_Reload(t *testing.T) {
	cleanup, _, m := newTestManager(t, NewSelfSignedCertGenerator())
	defer cleanup()

	// Data race: we read data owned by another goroutine!
	if len(m.certs) < 1 {
		t.Fatal("configuration not loaded?")
	}
	if m.certs[0].cn() != "example.com" {
		t.Fatalf("certs[0].cn() is %s, expected example.com", m.certs[0].cn())
	}

	// Try a reload, catch obvious errors.
	m.Reload()
	time.Sleep(50 * time.Millisecond)

	if len(m.certs) != 1 {
		t.Fatalf("certs count is %d, expected 1", len(m.certs))
	}
	if m.certs[0].cn() != "example.com" {
		t.Fatalf("certs[0].cn() is %s, expected example.com", m.certs[0].cn())
	}
}

func TestManager_NewCert(t *testing.T) {
	cleanup, _, m := newTestManager(t, NewSelfSignedCertGenerator())
	defer cleanup()

	now := time.Now()
	ci := m.certs[0]
	if ci.retryDeadline.After(now) {
		t.Fatalf("retry deadline is in the future: %v", ci.retryDeadline)
	}

	testUpdateCh <- true
	time.Sleep(100 * time.Millisecond)

	// Verify that the retry/renewal timestamp is in the future.
	if ci.retryDeadline.Before(now) {
		t.Fatalf("retry deadline is in the past after renewal: %v", ci.retryDeadline)
	}

	// Do we think we have a valid certificate?
	if !ci.valid {
		t.Fatal("we don't have a valid certificate")
	}

	// Verify that the credentials have successfully been written
	// to storage.
	p := filepath.Join(m.configDirs[0], "../certs/example.com/cert.pem")
	if _, err := os.Stat(p); err != nil {
		t.Fatalf("file not created: %v", err)
	}
	p = filepath.Join(m.configDirs[0], "../certs/example.com/private_key.pem")
	if _, err := os.Stat(p); err != nil {
		t.Fatalf("file not created: %v", err)
	}

	// By triggering a reload now, we should cause the Manager to
	// reload the certificate from storage.
	m.Reload()
	time.Sleep(50 * time.Millisecond)

	ci = m.certs[0]
	if !ci.valid {
		t.Fatal("certificate is invalid after a reload")
	}
}

func TestManager_CancelUpdate(t *testing.T) {
	start := time.Now()
	cleanup, cancel, m := newTestManager(t, &slowACME{})
	defer cleanup()

	time.Sleep(100 * time.Millisecond)
	cancel()
	m.Wait()
	elapsed := time.Since(start)
	if elapsed > 1*time.Second {
		t.Fatalf("too much time elapsed (%fs), canceling didn't work?", elapsed.Seconds())
	}
}