package malwarescan

import (
	"fmt"
	piperhttp "github.com/SAP/jenkins-library/pkg/http"
	"github.com/stretchr/testify/assert"
	"io"
	"io/ioutil"
	"net/http"
	"testing"
)

func TestMalwareServiceScan(t *testing.T) {
	t.Run("Scan without finding", func(t *testing.T) {
		httpClient := &httpMock{StatusCode: 200, ResponseBody: "{\"malwareDetected\":false,\"encryptedContentDetected\":false,\"scanSize\":298782,\"mimeType\":\"application/octet-stream\",\"SHA256\":\"96ca802fbd54d31903f1115a1d95590c685160637d9262bd340ab30d0f817e85\"}"}

		malwareService := ClientImpl{
			HTTPClient: httpClient,
			Host:       "https://example.org/malwarescanner",
		}

		candidate := readCloserMock{Content: "HELLO"}
		scanResult, err := malwareService.Scan(candidate)

		if assert.NoError(t, err) {
			assert.True(t, httpClient.Body.Closed)

			assert.Equal(t, "https://example.org/malwarescanner/scan", httpClient.URL)
			assert.Equal(t, "POST", httpClient.Method)

			if assert.NotNil(t, httpClient.Header) {
				assert.Equal(t, "application/octet-stream", httpClient.Header.Get("Content-Type"))
			}

			assert.Equal(t, "application/octet-stream", scanResult.MimeType)
			assert.Equal(t, 298782, scanResult.ScanSize)
			assert.Equal(t, "96ca802fbd54d31903f1115a1d95590c685160637d9262bd340ab30d0f817e85", scanResult.SHA256)
			assert.Equal(t, "", scanResult.Finding)
			assert.False(t, scanResult.MalwareDetected)
			assert.False(t, scanResult.EncryptedContentDetected)
		}
	})

	t.Run("Scan without finding", func(t *testing.T) {
		httpClient := &httpMock{StatusCode: 200, ResponseBody: "{\"malwareDetected\":true,\"encryptedContentDetected\":true,\"scanSize\":298782,\"mimeType\":\"application/octet-stream\",\"SHA256\":\"96ca802fbd54d31903f1115a1d95590c685160637d9262bd340ab30d0f817e85\", \"finding\": \"Description of the finding\"}"}

		malwareService := ClientImpl{
			HTTPClient: httpClient,
			Host:       "https://example.org/malwarescanner",
		}

		candidate := readCloserMock{Content: "HELLO"}
		scanResult, err := malwareService.Scan(candidate)

		if assert.NoError(t, err) {
			assert.True(t, httpClient.Body.Closed)

			assert.Equal(t, "https://example.org/malwarescanner/scan", httpClient.URL)
			assert.Equal(t, "POST", httpClient.Method)

			if assert.NotNil(t, httpClient.Header) {
				assert.Equal(t, "application/octet-stream", httpClient.Header.Get("Content-Type"))
			}

			assert.Equal(t, "application/octet-stream", scanResult.MimeType)
			assert.Equal(t, 298782, scanResult.ScanSize)
			assert.Equal(t, "96ca802fbd54d31903f1115a1d95590c685160637d9262bd340ab30d0f817e85", scanResult.SHA256)
			assert.Equal(t, "Description of the finding", scanResult.Finding)
			assert.True(t, scanResult.MalwareDetected)
			assert.True(t, scanResult.EncryptedContentDetected)
		}
	})

	t.Run("Scan results in error - file to large", func(t *testing.T) {
		httpClient := &httpMock{StatusCode: 413, ResponseBody: "{\"message\":\"Payload too large - The file is too large and cannot be scanned or the archive structure is too complex.\"}"}

		malwareService := ClientImpl{
			HTTPClient: httpClient,
			Host:       "https://example.org/malwarescanner",
		}

		candidate := readCloserMock{Content: "HELLO"}
		scanResult, err := malwareService.Scan(candidate)

		assert.Nil(t, scanResult)
		assert.EqualError(t, err, "MalwareService returned with status code 413: Payload too large - The file is too large and cannot be scanned or the archive structure is too complex.")
	})

	t.Run("Scan results in error - unexpected error", func(t *testing.T) {
		httpClient := &httpMock{StatusCode: 500, ResponseBody: ""}

		malwareService := ClientImpl{
			HTTPClient: httpClient,
			Host:       "https://example.org/malwarescanner",
		}

		candidate := readCloserMock{Content: "HELLO"}
		scanResult, err := malwareService.Scan(candidate)

		assert.Nil(t, scanResult)
		assert.EqualError(t, err, "MalwareService returned with status code 500, no further information available")
	})
}

func TestMalwareServiceInfo(t *testing.T) {
	t.Run("Receives engine info", func(t *testing.T) {
		httpClient := &httpMock{StatusCode: 200, ResponseBody: "{\"engineVersion\": \"Malware Service Mock\", \"signatureTimestamp\": \"2022-01-12T09:26:28.000Z\", \"maxScanSize\": 666}"}

		malwareService := ClientImpl{
			HTTPClient: httpClient,
			Host:       "https://example.org/malwarescanner",
		}

		info, err := malwareService.Info()

		if assert.NoError(t, err) {
			assert.True(t, httpClient.Body.Closed)

			assert.Equal(t, "https://example.org/malwarescanner/info", httpClient.URL)
			assert.Equal(t, "GET", httpClient.Method)
			assert.Equal(t, "Malware Service Mock", info.EngineVersion)
			assert.Equal(t, "2022-01-12T09:26:28.000Z", info.SignatureTimestamp)
			assert.Equal(t, 666, info.MaxScanSize)
		}
	})
}

type httpMock struct {
	Method       string                  // is set during test execution
	URL          string                  // is set before test execution
	ResponseBody string                  // is set before test execution
	Options      piperhttp.ClientOptions // is set during test
	StatusCode   int                     // is set during test
	Body         readCloserMock          // is set during test
	Header       http.Header             // is set during test
}

func (c *httpMock) SetOptions(options piperhttp.ClientOptions) {
	c.Options = options
}

func (c *httpMock) SendRequest(method string, url string, r io.Reader, header http.Header, cookies []*http.Cookie) (*http.Response, error) {
	c.Method = method
	c.URL = url
	c.Header = header

	if r != nil {
		_, err := ioutil.ReadAll(r)

		if err != nil {
			return nil, err
		}
	}

	c.Body = readCloserMock{Content: c.ResponseBody}
	res := http.Response{StatusCode: c.StatusCode, Body: &c.Body}

	return &res, nil
}

type readCloserMock struct {
	Content string
	Closed  bool
}

func (rc readCloserMock) Read(b []byte) (n int, err error) {

	if len(b) < len(rc.Content) {
		// in real life we would fill the buffer according to buffer size ...
		return 0, fmt.Errorf("Buffer size (%d) not sufficient, need: %d", len(b), len(rc.Content))
	}
	copy(b, rc.Content)
	return len(rc.Content), io.EOF
}

func (rc *readCloserMock) Close() error {
	rc.Closed = true
	return nil
}
