/*
Copyright IBM Corp. 2017 All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package server

import (
	"context"
	"testing"
	"time"

	"github.com/hyperledger/fabric-lib-go/common/metrics/disabled"
	cb "github.com/hyperledger/fabric-protos-go-apiv2/common"
	ab "github.com/hyperledger/fabric-protos-go-apiv2/orderer"
	"github.com/hyperledger/fabric/common/ledger/blockledger"
	"github.com/hyperledger/fabric/common/ledger/blockledger/fileledger"
	"github.com/hyperledger/fabric/common/util"
	"github.com/hyperledger/fabric/orderer/common/blockcutter"
	"github.com/hyperledger/fabric/orderer/common/msgprocessor"
	"github.com/hyperledger/fabric/orderer/common/multichannel"
	"github.com/hyperledger/fabric/orderer/common/multichannel/mocks"
	"github.com/hyperledger/fabric/orderer/consensus"
	"github.com/hyperledger/fabric/protoutil"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/require"
	"google.golang.org/grpc"
)

type AttestationServerStream struct {
	grpc.ServerStream
	ctx        context.Context
	ServerChan chan *ab.BlockAttestationResponse
}

func NewAttestationServerStream() *AttestationServerStream {
	return &AttestationServerStream{
		ctx:        context.Background(),
		ServerChan: make(chan *ab.BlockAttestationResponse, 10),
	}
}

func (ss *AttestationServerStream) Context() context.Context {
	return ss.ctx
}

func (ss *AttestationServerStream) Send(resp *ab.BlockAttestationResponse) error {
	ss.ServerChan <- resp
	return nil
}

func (ss *AttestationServerStream) RecvResponse() (*ab.BlockAttestationResponse, error) {
	response, more := <-ss.ServerChan
	if !more {
		return nil, errors.New("empty")
	}
	return response, nil
}

func newLedgerAndFactory(dir string, chainID string, genesisBlockSys *cb.Block) (blockledger.Factory, blockledger.ReadWriter) {
	rlf := newFactory(dir)
	rl := newLedger(rlf, chainID, genesisBlockSys)
	return rlf, rl
}

func newFactory(dir string) blockledger.Factory {
	rlf, err := fileledger.New(dir, &disabled.Provider{})
	if err != nil {
		panic(err)
	}

	return rlf
}

func newLedger(rlf blockledger.Factory, chainID string, genesisBlockSys *cb.Block) blockledger.ReadWriter {
	rl, err := rlf.GetOrCreate(chainID)
	if err != nil {
		panic(err)
	}

	if genesisBlockSys != nil {
		err = rl.Append(genesisBlockSys)
		if err != nil {
			panic(err)
		}
	}
	return rl
}

func mockCrypto() *mocks.SignerSerializer {
	return &mocks.SignerSerializer{}
}

type mockChainCluster struct {
	*mockChain
}

type mockChain struct {
	queue    chan *cb.Envelope
	cutter   blockcutter.Receiver
	support  consensus.ConsenterSupport
	metadata *cb.Metadata
	done     chan struct{}
}

func (mch *mockChain) Errored() <-chan struct{} {
	return nil
}

func (mch *mockChain) Order(env *cb.Envelope, configSeq uint64) error {
	mch.queue <- env
	return nil
}

func (mch *mockChain) Configure(config *cb.Envelope, configSeq uint64) error {
	mch.queue <- config
	return nil
}

func (mch *mockChain) WaitReady() error {
	return nil
}

func (mch *mockChain) Start() {
	go func() {
		defer close(mch.done)
		for {
			msg, ok := <-mch.queue
			if !ok {
				return
			}

			chdr, err := protoutil.ChannelHeader(msg)
			if err != nil {
				logger.Panicf("If a message has arrived to this point, it should already have had header inspected once: %s", err)
			}

			class := mch.support.ClassifyMsg(chdr)
			switch class {
			case msgprocessor.ConfigMsg:
				batch := mch.support.BlockCutter().Cut()
				if batch != nil {
					block := mch.support.CreateNextBlock(batch)
					mch.support.WriteBlock(block, nil)
				}

				_, err := mch.support.ProcessNormalMsg(msg)
				if err != nil {
					logger.Warningf("Discarding bad config message: %s", err)
					continue
				}
				block := mch.support.CreateNextBlock([]*cb.Envelope{msg})
				mch.support.WriteConfigBlock(block, nil)
			case msgprocessor.NormalMsg:
				batches, _ := mch.support.BlockCutter().Ordered(msg)
				for _, batch := range batches {
					block := mch.support.CreateNextBlock(batch)
					mch.support.WriteBlock(block, nil)
				}
			case msgprocessor.ConfigUpdateMsg:
				logger.Panicf("Not expecting msg class ConfigUpdateMsg here")
			default:
				logger.Panicf("Unsupported msg classification: %v", class)
			}
		}
	}()
}

func (mch *mockChain) Halt() {
	close(mch.queue)
}

func handleChainCluster(support consensus.ConsenterSupport, metadata *cb.Metadata) (consensus.Chain, error) {
	chain := &mockChain{
		queue:    make(chan *cb.Envelope),
		cutter:   support.BlockCutter(),
		support:  support,
		metadata: metadata,
		done:     make(chan struct{}),
	}

	return &mockChainCluster{mockChain: chain}, nil
}

func Test_attestationserver_BlockAttestations(t *testing.T) {
	ts := util.CreateUtcTimestamp()
	channelHeader := &cb.ChannelHeader{
		ChannelId: "mychannel",
		Timestamp: ts,
	}
	seekInfo := &ab.SeekInfo{
		Start: &ab.SeekPosition{
			Type: &ab.SeekPosition_Specified{
				Specified: &ab.SeekSpecified{Number: 0},
			},
		},
		Stop: &ab.SeekPosition{
			Type: &ab.SeekPosition_Specified{
				Specified: &ab.SeekSpecified{Number: 0},
			},
		},
	}
	envelope := &cb.Envelope{}
	payload := &cb.Payload{
		Header: &cb.Header{
			ChannelHeader:   protoutil.MarshalOrPanic(channelHeader),
			SignatureHeader: protoutil.MarshalOrPanic(&cb.SignatureHeader{}),
		},
		Data: protoutil.MarshalOrPanic(seekInfo),
	}
	envelope.Payload = protoutil.MarshalOrPanic(payload)

	// TODO This is preparation work for the BFT block puller. Right now it is used only in unit tests.
	// We need to update these tests.
	t.Run("Complete block attestation is successfully", func(t *testing.T) {
		t.Skip("Work in progress. BFT Block Puller preparatory work. This test needs to be rewritten.")
		// TODO
	})

	t.Run("Block attestation returns with BAD_REQUEST when envelope is malformed", func(t *testing.T) {
		mockstream := NewAttestationServerStream()
		asr := NewAttestationService(&multichannel.Registrar{}, &disabled.Provider{}, nil, time.Second, false, false)
		err := asr.BlockAttestations(&cb.Envelope{}, mockstream)
		require.NoError(t, err)
		StatusResponse, _ := mockstream.RecvResponse()
		require.Equal(t, cb.Status_BAD_REQUEST, StatusResponse.GetStatus())
	})

	t.Run("Block attestion returns with NOTFOUND when custom policy check fails", func(t *testing.T) {
		mockstream := NewAttestationServerStream()
		asr := NewAttestationService(&multichannel.Registrar{}, &disabled.Provider{}, nil, time.Second, false, false)
		err := asr.BlockAttestations(envelope, mockstream)
		require.NoError(t, err)
		StatusResponse, _ := mockstream.RecvResponse()
		require.Equal(t, cb.Status_NOT_FOUND, StatusResponse.GetStatus())
	})
}
