/*
Copyright IBM Corp All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package fabhttp_test

import (
	"crypto/tls"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"syscall"

	"github.com/hyperledger/fabric/common/fabhttp"
	"github.com/hyperledger/fabric/core/operations/fakes"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/tedsuo/ifrit"
)

var _ = Describe("Server", func() {
	const AdditionalTestApiPath = "/some-additional-test-api"

	var (
		fakeLogger *fakes.Logger
		tempDir    string

		client       *http.Client
		unauthClient *http.Client
		options      fabhttp.Options
		server       *fabhttp.Server
	)

	BeforeEach(func() {
		var err error
		tempDir, err = os.MkdirTemp("", "fabhttp-test")
		Expect(err).NotTo(HaveOccurred())

		generateCertificates(tempDir)
		client = newHTTPClient(tempDir, true)
		unauthClient = newHTTPClient(tempDir, false)

		fakeLogger = &fakes.Logger{}
		options = fabhttp.Options{
			Logger:        fakeLogger,
			ListenAddress: "127.0.0.1:0",
			TLS: fabhttp.TLS{
				Enabled:            true,
				CertFile:           filepath.Join(tempDir, "server-cert.pem"),
				KeyFile:            filepath.Join(tempDir, "server-key.pem"),
				ClientCertRequired: false,
				ClientCACertFiles:  []string{filepath.Join(tempDir, "client-ca.pem")},
			},
		}

		server = fabhttp.NewServer(options)
	})

	AfterEach(func() {
		os.RemoveAll(tempDir)
		if server != nil {
			server.Stop()
		}
	})

	When("trying to connect with an old TLS version", func() {
		BeforeEach(func() {
			tlsOpts := []func(config *tls.Config){func(config *tls.Config) {
				config.MaxVersion = tls.VersionTLS11
				config.ClientAuth = tls.RequireAndVerifyClientCert
			}}

			client = newHTTPClient(tempDir, true, tlsOpts...)
		})

		It("does not answer clients using an older TLS version than 1.2", func() {
			server.RegisterHandler(AdditionalTestApiPath, &fakes.Handler{Code: http.StatusOK, Text: "secure"}, options.TLS.Enabled)
			err := server.Start()
			Expect(err).NotTo(HaveOccurred())

			addApiURL := fmt.Sprintf("https://%s%s", server.Addr(), AdditionalTestApiPath)
			_, err = client.Get(addApiURL)
			Expect(err.Error()).To(ContainSubstring("tls: no supported versions satisfy MinVersion and MaxVersion"))
		})
	})

	It("does not host a secure endpoint for additional APIs by default", func() {
		err := server.Start()
		Expect(err).NotTo(HaveOccurred())

		addApiURL := fmt.Sprintf("https://%s%s", server.Addr(), AdditionalTestApiPath)
		resp, err := client.Get(addApiURL)
		Expect(err).NotTo(HaveOccurred())
		Expect(resp.StatusCode).To(Equal(http.StatusNotFound)) // service is not handled by default, i.e. in peer
		resp.Body.Close()

		resp, err = unauthClient.Get(addApiURL)
		Expect(err).NotTo(HaveOccurred())
		Expect(resp.StatusCode).To(Equal(http.StatusNotFound))
	})

	It("hosts a secure endpoint for additional APIs when added", func() {
		server.RegisterHandler(AdditionalTestApiPath, &fakes.Handler{Code: http.StatusOK, Text: "secure"}, options.TLS.Enabled)
		err := server.Start()
		Expect(err).NotTo(HaveOccurred())

		addApiURL := fmt.Sprintf("https://%s%s", server.Addr(), AdditionalTestApiPath)
		resp, err := client.Get(addApiURL)
		Expect(err).NotTo(HaveOccurred())
		Expect(resp.StatusCode).To(Equal(http.StatusOK))
		Expect(resp.Header.Get("Content-Type")).To(Equal("text/plain; charset=utf-8"))
		buff, err := io.ReadAll(resp.Body)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(buff)).To(Equal("secure"))
		resp.Body.Close()

		resp, err = unauthClient.Get(addApiURL)
		Expect(err).NotTo(HaveOccurred())
		Expect(resp.StatusCode).To(Equal(http.StatusUnauthorized))
	})

	Context("when TLS is disabled", func() {
		BeforeEach(func() {
			options.TLS.Enabled = false
			server = fabhttp.NewServer(options)
		})

		It("does not host an insecure endpoint for additional APIs by default", func() {
			err := server.Start()
			Expect(err).NotTo(HaveOccurred())

			addApiURL := fmt.Sprintf("http://%s%s", server.Addr(), AdditionalTestApiPath)
			resp, err := client.Get(addApiURL)
			Expect(err).NotTo(HaveOccurred())
			Expect(resp.StatusCode).To(Equal(http.StatusNotFound)) // service is not handled by default, i.e. in peer
			resp.Body.Close()
		})

		It("hosts an insecure endpoint for additional APIs when added", func() {
			server.RegisterHandler(AdditionalTestApiPath, &fakes.Handler{Code: http.StatusOK, Text: "insecure"}, options.TLS.Enabled)
			err := server.Start()
			Expect(err).NotTo(HaveOccurred())

			addApiURL := fmt.Sprintf("http://%s%s", server.Addr(), AdditionalTestApiPath)
			resp, err := client.Get(addApiURL)
			Expect(err).NotTo(HaveOccurred())
			Expect(resp.StatusCode).To(Equal(http.StatusOK))
			Expect(resp.Header.Get("Content-Type")).To(Equal("text/plain; charset=utf-8"))
			buff, err := io.ReadAll(resp.Body)
			Expect(err).NotTo(HaveOccurred())
			Expect(string(buff)).To(Equal("insecure"))
			resp.Body.Close()
		})
	})

	Context("when ClientCertRequired is true", func() {
		BeforeEach(func() {
			options.TLS.ClientCertRequired = true
			server = fabhttp.NewServer(options)
		})

		It("requires a client cert to connect", func() {
			err := server.Start()
			Expect(err).NotTo(HaveOccurred())

			_, err = unauthClient.Get(fmt.Sprintf("https://%s/healthz", server.Addr()))
			Expect(err).To(BeAssignableToTypeOf(&url.Error{}))
			Expect(err.(*url.Error).Err.Error()).To(ContainSubstring("remote error: tls: certificate required"))
		})
	})

	Context("when listen fails", func() {
		var listener net.Listener

		BeforeEach(func() {
			var err error
			listener, err = net.Listen("tcp", "127.0.0.1:0")
			Expect(err).NotTo(HaveOccurred())

			options.ListenAddress = listener.Addr().String()
			server = fabhttp.NewServer(options)
		})

		AfterEach(func() {
			listener.Close()
		})

		It("returns an error", func() {
			err := server.Start()
			Expect(err).To(MatchError(ContainSubstring("bind: address already in use")))
		})
	})

	Context("when a bad TLS configuration is provided", func() {
		BeforeEach(func() {
			options.TLS.CertFile = "cert-file-does-not-exist"
			server = fabhttp.NewServer(options)
		})

		It("returns an error", func() {
			err := server.Start()
			Expect(err).To(MatchError("open cert-file-does-not-exist: no such file or directory"))
		})
	})

	It("proxies Log to the provided logger", func() {
		err := server.Log("key", "value")
		Expect(err).NotTo(HaveOccurred())

		Expect(fakeLogger.WarnCallCount()).To(Equal(1))
		Expect(fakeLogger.WarnArgsForCall(0)).To(Equal([]interface{}{"key", "value"}))
	})

	Context("when a logger is not provided", func() {
		BeforeEach(func() {
			options.Logger = nil
			server = fabhttp.NewServer(options)
		})

		It("does not panic when logging", func() {
			Expect(func() { server.Log("key", "value") }).NotTo(Panic())
		})

		It("returns nil from Log", func() {
			err := server.Log("key", "value")
			Expect(err).NotTo(HaveOccurred())
		})
	})

	It("supports ifrit", func() {
		process := ifrit.Invoke(server)
		Eventually(process.Ready()).Should(BeClosed())

		process.Signal(syscall.SIGTERM)
		Eventually(process.Wait()).Should(Receive(BeNil()))
	})

	Context("when start fails and ifrit is used", func() {
		BeforeEach(func() {
			options.TLS.CertFile = "non-existent-file"
			server = fabhttp.NewServer(options)
		})

		It("does not close the ready chan", func() {
			process := ifrit.Invoke(server)
			Consistently(process.Ready()).ShouldNot(BeClosed())
			Eventually(process.Wait()).Should(Receive(MatchError("open non-existent-file: no such file or directory")))
		})
	})
})
