package sa

import (
	"bytes"
	"crypto/sha256"
	"fmt"
	"testing"
	"time"

	"github.com/letsencrypt/boulder/db"
	berrors "github.com/letsencrypt/boulder/errors"
	sapb "github.com/letsencrypt/boulder/sa/proto"
	"github.com/letsencrypt/boulder/sa/satest"
	"github.com/letsencrypt/boulder/test"
)

// findIssuedName is a small helper test function to directly query the
// issuedNames table for a given name to find a serial (or return an err).
func findIssuedName(dbMap db.OneSelector, name string) (string, error) {
	var issuedNamesSerial string
	err := dbMap.SelectOne(
		&issuedNamesSerial,
		`SELECT serial FROM issuedNames
		WHERE reversedName = ?
		ORDER BY notBefore DESC
		LIMIT 1`,
		ReverseName(name))
	return issuedNamesSerial, err
}

func TestAddPrecertificate(t *testing.T) {
	sa, clk, cleanUp := initSA(t)
	defer cleanUp()

	reg := satest.CreateWorkingRegistration(t, sa)

	addPrecert := func(expectIssuedNamesUpdate bool) {
		// Create a throw-away self signed certificate with a random name and
		// serial number
		serial, testCert := test.ThrowAwayCert(t, 1)

		// Add the cert as a precertificate
		ocspResp := []byte{0, 0, 1}
		regID := reg.ID
		issuedTime := time.Date(2018, 4, 1, 7, 0, 0, 0, time.UTC)
		_, err := sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{
			Der:      testCert.Raw,
			RegID:    regID,
			Ocsp:     ocspResp,
			Issued:   issuedTime.UnixNano(),
			IssuerID: 1,
		})
		test.AssertNotError(t, err, "Couldn't add test cert")

		// It should have the expected certificate status
		certStatus, err := sa.GetCertificateStatus(ctx, serial)
		test.AssertNotError(t, err, "Couldn't get status for test cert")
		test.Assert(
			t,
			bytes.Compare(certStatus.OCSPResponse, ocspResp) == 0,
			fmt.Sprintf("OCSP responses don't match, expected: %x, got %x", certStatus.OCSPResponse, ocspResp),
		)
		test.Assert(
			t,
			clk.Now().Equal(certStatus.OCSPLastUpdated),
			fmt.Sprintf("OCSPLastUpdated doesn't match, expected %s, got %s", clk.Now(), certStatus.OCSPLastUpdated),
		)

		issuedNamesSerial, err := findIssuedName(sa.dbMap, testCert.DNSNames[0])
		if expectIssuedNamesUpdate {
			// If we expectIssuedNamesUpdate then there should be no err and the
			// expected serial
			test.AssertNotError(t, err, "expected no err querying issuedNames for precert")
			test.AssertEquals(t, issuedNamesSerial, serial)

			// We should also be able to call AddCertificate with the same cert
			// without it being an error. The duplicate err on inserting to
			// issuedNames should be ignored.
			_, err := sa.AddCertificate(ctx, testCert.Raw, regID, nil, &issuedTime)
			test.AssertNotError(t, err, "unexpected err adding final cert after precert")
		} else {
			// Otherwise we expect an ErrDatabaseOp that indicates NoRows because
			// AddCertificate not AddPrecertificate will be updating this table.
			test.AssertEquals(t, db.IsNoRows(err), true)
		}

		// Adding the same certificate with the same serial should result in an
		// error
		_, err = sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{
			Der:      testCert.Raw,
			RegID:    regID,
			Ocsp:     ocspResp,
			Issued:   issuedTime.UnixNano(),
			IssuerID: 1,
		})
		test.AssertError(t, err, "Expected error inserting duplicate precertificate")
		test.AssertErrorIs(t, err, berrors.Duplicate)
	}

	addPrecert(true)
}

func TestAddPrecertificateIncomplete(t *testing.T) {
	sa, _, cleanUp := initSA(t)
	defer cleanUp()

	reg := satest.CreateWorkingRegistration(t, sa)

	// Create a throw-away self signed certificate with a random name and
	// serial number
	_, testCert := test.ThrowAwayCert(t, 1)

	// Add the cert as a precertificate
	ocspResp := []byte{0, 0, 1}
	regID := reg.ID
	_, err := sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{
		Der:    testCert.Raw,
		RegID:  regID,
		Ocsp:   ocspResp,
		Issued: time.Date(2018, 4, 1, 7, 0, 0, 0, time.UTC).UnixNano(),
		// Leaving out IssuerID
	})

	test.AssertError(t, err, "Adding precert with no issuer did not fail")
}

func TestAddPrecertificateKeyHash(t *testing.T) {
	sa, _, cleanUp := initSA(t)
	defer cleanUp()
	reg := satest.CreateWorkingRegistration(t, sa)

	serial, testCert := test.ThrowAwayCert(t, 1)
	_, err := sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{
		Der:      testCert.Raw,
		RegID:    reg.ID,
		Ocsp:     []byte{1, 2, 3},
		Issued:   testCert.NotBefore.UnixNano(),
		IssuerID: 1,
	})
	test.AssertNotError(t, err, "failed to add precert")

	var keyHashes []keyHashModel
	_, err = sa.dbMap.Select(&keyHashes, "SELECT * FROM keyHashToSerial")
	test.AssertNotError(t, err, "failed to retrieve rows from keyHashToSerial")
	test.AssertEquals(t, len(keyHashes), 1)
	test.AssertEquals(t, keyHashes[0].CertSerial, serial)
	test.AssertEquals(t, keyHashes[0].CertNotAfter, testCert.NotAfter)
	spkiHash := sha256.Sum256(testCert.RawSubjectPublicKeyInfo)
	test.Assert(t, bytes.Compare(keyHashes[0].KeyHash, spkiHash[:]) == 0, "spki hash mismatch")
}
