package ca

import (
	"crypto/x509"
	"fmt"
	"io"
	"testing"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/protobuf/types/known/timestamppb"

	capb "github.com/letsencrypt/boulder/ca/proto"
	"github.com/letsencrypt/boulder/config"
	corepb "github.com/letsencrypt/boulder/core/proto"
	"github.com/letsencrypt/boulder/issuance"
	"github.com/letsencrypt/boulder/test"
)

type mockGenerateCRLBidiStream struct {
	grpc.ServerStream
	input  <-chan *capb.GenerateCRLRequest
	output chan<- *capb.GenerateCRLResponse
}

func (s mockGenerateCRLBidiStream) Recv() (*capb.GenerateCRLRequest, error) {
	next, ok := <-s.input
	if !ok {
		return nil, io.EOF
	}
	return next, nil
}

func (s mockGenerateCRLBidiStream) Send(entry *capb.GenerateCRLResponse) error {
	s.output <- entry
	return nil
}

func TestGenerateCRL(t *testing.T) {
	t.Parallel()
	cargs := newCAArgs(t)
	crli, err := NewCRLImpl(
		cargs.issuers,
		issuance.CRLProfileConfig{
			ValidityInterval: config.Duration{Duration: 216 * time.Hour},
			MaxBackdate:      config.Duration{Duration: time.Hour},
		},
		100,
		cargs.logger,
		cargs.metrics,
	)
	test.AssertNotError(t, err, "Failed to create crl impl")
	errs := make(chan error, 1)

	// Test that we get an error when no metadata is sent.
	ins := make(chan *capb.GenerateCRLRequest)
	go func() {
		errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{input: ins, output: nil})
	}()
	close(ins)
	err = <-errs
	test.AssertError(t, err, "can't generate CRL with no metadata")
	test.AssertContains(t, err.Error(), "no crl metadata received")

	// Test that we get an error when incomplete metadata is sent.
	ins = make(chan *capb.GenerateCRLRequest)
	go func() {
		errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{input: ins, output: nil})
	}()
	ins <- &capb.GenerateCRLRequest{
		Payload: &capb.GenerateCRLRequest_Metadata{
			Metadata: &capb.CRLMetadata{},
		},
	}
	close(ins)
	err = <-errs
	test.AssertError(t, err, "can't generate CRL with incomplete metadata")
	test.AssertContains(t, err.Error(), "got incomplete metadata message")

	// Test that we get an error when unrecognized metadata is sent.
	ins = make(chan *capb.GenerateCRLRequest)
	go func() {
		errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{input: ins, output: nil})
	}()
	now := cargs.clk.Now()
	ins <- &capb.GenerateCRLRequest{
		Payload: &capb.GenerateCRLRequest_Metadata{
			Metadata: &capb.CRLMetadata{
				IssuerNameID: 1,
				ThisUpdate:   timestamppb.New(now),
				ShardIdx:     1,
			},
		},
	}
	close(ins)
	err = <-errs
	test.AssertError(t, err, "can't generate CRL with bad metadata")
	test.AssertContains(t, err.Error(), "got unrecognized IssuerNameID")

	// Test that we get an error when two metadata are sent.
	ins = make(chan *capb.GenerateCRLRequest)
	go func() {
		errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{input: ins, output: nil})
	}()
	ins <- &capb.GenerateCRLRequest{
		Payload: &capb.GenerateCRLRequest_Metadata{
			Metadata: &capb.CRLMetadata{
				IssuerNameID: int64(cargs.issuers[0].NameID()),
				ThisUpdate:   timestamppb.New(now),
				ShardIdx:     1,
			},
		},
	}
	ins <- &capb.GenerateCRLRequest{
		Payload: &capb.GenerateCRLRequest_Metadata{
			Metadata: &capb.CRLMetadata{
				IssuerNameID: int64(cargs.issuers[0].NameID()),
				ThisUpdate:   timestamppb.New(now),
				ShardIdx:     1,
			},
		},
	}
	close(ins)
	err = <-errs
	fmt.Println("done waiting for error")
	test.AssertError(t, err, "can't generate CRL with duplicate metadata")
	test.AssertContains(t, err.Error(), "got more than one metadata message")

	// Test that we get an error when an entry has a bad serial.
	ins = make(chan *capb.GenerateCRLRequest)
	go func() {
		errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{input: ins, output: nil})
	}()
	ins <- &capb.GenerateCRLRequest{
		Payload: &capb.GenerateCRLRequest_Entry{
			Entry: &corepb.CRLEntry{
				Serial:    "123",
				Reason:    1,
				RevokedAt: timestamppb.New(now),
			},
		},
	}
	close(ins)
	err = <-errs
	test.AssertError(t, err, "can't generate CRL with bad serials")
	test.AssertContains(t, err.Error(), "invalid serial number")

	// Test that we get an error when an entry has a bad revocation time.
	ins = make(chan *capb.GenerateCRLRequest)
	go func() {
		errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{input: ins, output: nil})
	}()

	ins <- &capb.GenerateCRLRequest{
		Payload: &capb.GenerateCRLRequest_Entry{
			Entry: &corepb.CRLEntry{
				Serial:    "deadbeefdeadbeefdeadbeefdeadbeefdead",
				Reason:    1,
				RevokedAt: nil,
			},
		},
	}
	close(ins)
	err = <-errs
	test.AssertError(t, err, "can't generate CRL with bad serials")
	test.AssertContains(t, err.Error(), "got empty or zero revocation timestamp")

	// Test that generating an empty CRL works.
	ins = make(chan *capb.GenerateCRLRequest)
	outs := make(chan *capb.GenerateCRLResponse)
	go func() {
		errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{input: ins, output: outs})
		close(outs)
	}()
	crlBytes := make([]byte, 0)
	done := make(chan struct{})
	go func() {
		for resp := range outs {
			crlBytes = append(crlBytes, resp.Chunk...)
		}
		close(done)
	}()
	ins <- &capb.GenerateCRLRequest{
		Payload: &capb.GenerateCRLRequest_Metadata{
			Metadata: &capb.CRLMetadata{
				IssuerNameID: int64(cargs.issuers[0].NameID()),
				ThisUpdate:   timestamppb.New(now),
				ShardIdx:     1,
			},
		},
	}
	close(ins)
	err = <-errs
	<-done
	test.AssertNotError(t, err, "generating empty CRL should work")
	test.Assert(t, len(crlBytes) > 0, "should have gotten some CRL bytes")
	crl, err := x509.ParseRevocationList(crlBytes)
	test.AssertNotError(t, err, "should be able to parse empty CRL")
	test.AssertEquals(t, len(crl.RevokedCertificateEntries), 0)
	err = crl.CheckSignatureFrom(cargs.issuers[0].Cert.Certificate)
	test.AssertEquals(t, crl.ThisUpdate, now)
	test.AssertEquals(t, crl.ThisUpdate, timestamppb.New(now).AsTime())
	test.AssertNotError(t, err, "CRL signature should validate")

	// Test that generating a CRL with some entries works.
	ins = make(chan *capb.GenerateCRLRequest)
	outs = make(chan *capb.GenerateCRLResponse)
	go func() {
		errs <- crli.GenerateCRL(mockGenerateCRLBidiStream{input: ins, output: outs})
		close(outs)
	}()
	crlBytes = make([]byte, 0)
	done = make(chan struct{})
	go func() {
		for resp := range outs {
			crlBytes = append(crlBytes, resp.Chunk...)
		}
		close(done)
	}()
	ins <- &capb.GenerateCRLRequest{
		Payload: &capb.GenerateCRLRequest_Metadata{
			Metadata: &capb.CRLMetadata{
				IssuerNameID: int64(cargs.issuers[0].NameID()),
				ThisUpdate:   timestamppb.New(now),
				ShardIdx:     1,
			},
		},
	}
	ins <- &capb.GenerateCRLRequest{
		Payload: &capb.GenerateCRLRequest_Entry{
			Entry: &corepb.CRLEntry{
				Serial:    "000000000000000000000000000000000000",
				RevokedAt: timestamppb.New(now),
				// Reason 0, Unspecified, is omitted.
			},
		},
	}
	ins <- &capb.GenerateCRLRequest{
		Payload: &capb.GenerateCRLRequest_Entry{
			Entry: &corepb.CRLEntry{
				Serial:    "111111111111111111111111111111111111",
				Reason:    1, // keyCompromise
				RevokedAt: timestamppb.New(now),
			},
		},
	}
	ins <- &capb.GenerateCRLRequest{
		Payload: &capb.GenerateCRLRequest_Entry{
			Entry: &corepb.CRLEntry{
				Serial:    "444444444444444444444444444444444444",
				Reason:    4, // superseded
				RevokedAt: timestamppb.New(now),
			},
		},
	}
	ins <- &capb.GenerateCRLRequest{
		Payload: &capb.GenerateCRLRequest_Entry{
			Entry: &corepb.CRLEntry{
				Serial:    "555555555555555555555555555555555555",
				Reason:    5, // cessationOfOperation
				RevokedAt: timestamppb.New(now),
			},
		},
	}
	ins <- &capb.GenerateCRLRequest{
		Payload: &capb.GenerateCRLRequest_Entry{
			Entry: &corepb.CRLEntry{
				Serial:    "999999999999999999999999999999999999",
				Reason:    9, // privilegeWithdrawn
				RevokedAt: timestamppb.New(now),
			},
		},
	}
	close(ins)
	err = <-errs
	<-done
	test.AssertNotError(t, err, "generating empty CRL should work")
	test.Assert(t, len(crlBytes) > 0, "should have gotten some CRL bytes")
	crl, err = x509.ParseRevocationList(crlBytes)
	test.AssertNotError(t, err, "should be able to parse empty CRL")
	test.AssertEquals(t, len(crl.RevokedCertificateEntries), 5)
	err = crl.CheckSignatureFrom(cargs.issuers[0].Cert.Certificate)
	test.AssertNotError(t, err, "CRL signature should validate")
}
