/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package container_test

import (
	"bytes"
	"io"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"github.com/hyperledger/fabric/core/chaincode/persistence"
	"github.com/hyperledger/fabric/core/container"
	"github.com/hyperledger/fabric/core/container/ccintf"
	"github.com/hyperledger/fabric/core/container/mock"
	"github.com/pkg/errors"
)

var _ = Describe("Router", func() {
	var (
		fakeDockerBuilder   *mock.DockerBuilder
		fakeExternalBuilder *mock.ExternalBuilder
		fakePackageProvider *mock.PackageProvider
		fakeInstance        *mock.Instance
		router              *container.Router
	)

	BeforeEach(func() {
		fakeDockerBuilder = &mock.DockerBuilder{}
		fakeExternalBuilder = &mock.ExternalBuilder{}
		fakeInstance = &mock.Instance{}
		fakePackageProvider = &mock.PackageProvider{}
		fakePackageProvider.GetChaincodePackageReturns(
			&persistence.ChaincodePackageMetadata{
				Type: "package-type",
				Path: "package-path",
			},
			[]byte(`{"some":"json"}`),
			io.NopCloser(bytes.NewBuffer([]byte("code-bytes"))),
			nil,
		)

		router = &container.Router{
			DockerBuilder:   fakeDockerBuilder,
			ExternalBuilder: fakeExternalBuilder,
			PackageProvider: fakePackageProvider,
		}
	})

	Describe("Build", func() {
		BeforeEach(func() {
			fakeExternalBuilder.BuildReturns(fakeInstance, nil)
		})

		It("calls the external builder with the correct args", func() {
			err := router.Build("package-id")
			Expect(err).NotTo(HaveOccurred())

			Expect(fakeExternalBuilder.BuildCallCount()).To(Equal(1))
			ccid, md, codeStream := fakeExternalBuilder.BuildArgsForCall(0)
			Expect(ccid).To(Equal("package-id"))
			Expect(md).To(Equal([]byte(`{"some":"json"}`)))
			codePackage, err := io.ReadAll(codeStream)
			Expect(err).NotTo(HaveOccurred())
			Expect(codePackage).To(Equal([]byte("code-bytes")))
		})

		Context("when the package provider returns an error before calling the external builder", func() {
			BeforeEach(func() {
				fakePackageProvider.GetChaincodePackageReturns(nil, nil, nil, errors.New("fake-package-error"))
			})

			It("wraps and returns the error", func() {
				err := router.Build("package-id")
				Expect(err).To(MatchError("failed to get chaincode package for external build: fake-package-error"))
			})

			It("does not call the external builder", func() {
				router.Build("package-id")
				Expect(fakeExternalBuilder.BuildCallCount()).To(Equal(0))
			})
		})

		Context("when the external builder returns an error", func() {
			BeforeEach(func() {
				fakeExternalBuilder.BuildReturns(nil, errors.New("fake-external-error"))
				fakeDockerBuilder.BuildReturns(fakeInstance, nil)
			})

			It("wraps and returns the error", func() {
				err := router.Build("package-id")
				Expect(err).To(MatchError("external builder failed: fake-external-error"))
			})
		})

		Context("when the external builder returns successfully with an instance", func() {
			It("does not call the docker builder", func() {
				err := router.Build("package-id")
				Expect(err).NotTo(HaveOccurred())

				Expect(fakeExternalBuilder.BuildCallCount()).To(Equal(1))
				Expect(fakeDockerBuilder.BuildCallCount()).To(Equal(0))
			})
		})

		Context("when the external builder returns a nil instance", func() {
			BeforeEach(func() {
				fakeExternalBuilder.BuildReturns(nil, nil)
				fakeDockerBuilder.BuildReturns(fakeInstance, nil)
			})

			It("falls back to the docker impl", func() {
				err := router.Build("package-id")
				Expect(err).NotTo(HaveOccurred())

				Expect(fakeDockerBuilder.BuildCallCount()).To(Equal(1))
				ccid, md, codeStream := fakeDockerBuilder.BuildArgsForCall(0)
				Expect(ccid).To(Equal("package-id"))
				Expect(md).To(Equal(&persistence.ChaincodePackageMetadata{
					Type: "package-type",
					Path: "package-path",
				}))
				codePackage, err := io.ReadAll(codeStream)
				Expect(err).NotTo(HaveOccurred())
				Expect(codePackage).To(Equal([]byte("code-bytes")))
			})

			Context("when the docker vm builder is nil", func() {
				BeforeEach(func() {
					router.DockerBuilder = nil
				})

				It("returns the error", func() {
					err := router.Build("package-id")
					Expect(err).To(MatchError("no DockerBuilder, cannot build"))
				})
			})

			Context("when the package provider returns an error before calling the docker builder", func() {
				BeforeEach(func() {
					fakePackageProvider.GetChaincodePackageReturnsOnCall(1, nil, nil, nil, errors.New("fake-package-error"))
				})

				It("wraps and returns the error", func() {
					err := router.Build("package-id")
					Expect(err).To(MatchError("failed to get chaincode package for docker build: fake-package-error"))
				})
			})
		})

		Context("when an external builder is not provided", func() {
			BeforeEach(func() {
				router.ExternalBuilder = nil
				fakeDockerBuilder.BuildReturns(fakeInstance, nil)
			})

			It("uses the docker vm builder", func() {
				err := router.Build("package-id")
				Expect(err).NotTo(HaveOccurred())
				Expect(fakeDockerBuilder.BuildCallCount()).To(Equal(1))
			})
		})
	})

	Describe("Post-build operations", func() {
		BeforeEach(func() {
			fakeExternalBuilder.BuildReturns(fakeInstance, nil)
			err := router.Build("fake-id")
			Expect(err).NotTo(HaveOccurred())
		})

		Describe("Start", func() {
			BeforeEach(func() {
				fakeInstance.StartReturns(errors.New("fake-start-error"))
			})

			It("passes through to the docker impl", func() {
				err := router.Start(
					"fake-id",
					&ccintf.PeerConnection{
						Address: "peer-address",
						TLSConfig: &ccintf.TLSConfig{
							ClientKey:  []byte("key"),
							ClientCert: []byte("cert"),
							RootCert:   []byte("root"),
						},
					},
				)

				Expect(err).To(MatchError("fake-start-error"))
				Expect(fakeInstance.StartCallCount()).To(Equal(1))
				Expect(fakeInstance.StartArgsForCall(0)).To(Equal(&ccintf.PeerConnection{
					Address: "peer-address",
					TLSConfig: &ccintf.TLSConfig{
						ClientKey:  []byte("key"),
						ClientCert: []byte("cert"),
						RootCert:   []byte("root"),
					},
				}))
			})

			Context("when the chaincode has not yet been built", func() {
				It("returns an error", func() {
					err := router.Start(
						"missing-name",
						&ccintf.PeerConnection{
							Address: "peer-address",
						},
					)
					Expect(err).To(MatchError("instance has not yet been built, cannot be started"))
				})
			})
		})

		Describe("Stop", func() {
			BeforeEach(func() {
				fakeInstance.StopReturns(errors.New("Boo"))
			})

			It("passes through to the docker impl", func() {
				err := router.Stop("fake-id")
				Expect(err).To(MatchError("Boo"))
				Expect(fakeInstance.StopCallCount()).To(Equal(1))
			})

			Context("when the chaincode has not yet been built", func() {
				It("returns an error", func() {
					err := router.Stop("missing-name")
					Expect(err).To(MatchError("instance has not yet been built, cannot be stopped"))
				})
			})
		})

		Describe("Wait", func() {
			BeforeEach(func() {
				fakeInstance.WaitReturns(7, errors.New("fake-wait-error"))
			})

			It("passes through to the docker impl", func() {
				res, err := router.Wait(
					"fake-id",
				)
				Expect(res).To(Equal(7))
				Expect(err).To(MatchError("fake-wait-error"))
				Expect(fakeInstance.WaitCallCount()).To(Equal(1))
			})

			Context("when the chaincode has not yet been built", func() {
				It("returns an error", func() {
					_, err := router.Wait("missing-name")
					Expect(err).To(MatchError("instance has not yet been built, cannot wait"))
				})
			})
		})
	})
})
