package scanners_test

import (
	"bytes"
	"errors"
	"net/http"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"

	ic "github.com/opencloud-eu/icap-client"

	"github.com/qsfera/server/services/antivirus/pkg/scanners"
	"github.com/qsfera/server/services/antivirus/pkg/scanners/mocks"
)

func TestICAP_Scan(t *testing.T) {
	var (
		earlyExitErr = errors.New("stop here")
		testUrl      = "icap://test"
		client       = mocks.NewScanner(t)
		scanner      = &scanners.ICAP{Client: client, URL: testUrl}
	)

	t.Run("it sends a OPTIONS request to determine details", func(t *testing.T) {
		client.EXPECT().Do(mock.Anything).RunAndReturn(func(request ic.Request) (ic.Response, error) {
			assert.Equal(t, ic.MethodOPTIONS, request.Method)
			assert.Equal(t, testUrl, request.URL.String())
			return ic.Response{}, earlyExitErr
		}).Once()

		_, err := scanner.Scan(scanners.Input{})
		assert.ErrorIs(t, earlyExitErr, err) // we can exit early, just in case check the error to be identical to the early exit error
	})

	t.Run("it sends a REQMOD request with all the details", func(t *testing.T) {

		t.Run("request with ContentLength", func(t *testing.T) {
			t.Run("with size", func(t *testing.T) {
				client.EXPECT().Do(mock.Anything).Return(ic.Response{}, nil).Once()

				client.EXPECT().Do(mock.Anything).RunAndReturn(func(request ic.Request) (ic.Response, error) {
					assert.Equal(t, ic.MethodREQMOD, request.Method)
					assert.Equal(t, testUrl, request.URL.String())
					assert.EqualValues(t, 999, request.HTTPRequest.ContentLength)
					return ic.Response{}, earlyExitErr
				}).Once()

				_, err := scanner.Scan(scanners.Input{Size: 999})
				assert.ErrorIs(t, earlyExitErr, err)
			})

			t.Run("without size", func(t *testing.T) {
				client.EXPECT().Do(mock.Anything).Return(ic.Response{}, nil).Once()

				client.EXPECT().Do(mock.Anything).RunAndReturn(func(request ic.Request) (ic.Response, error) {
					assert.Equal(t, ic.MethodREQMOD, request.Method)
					assert.Equal(t, testUrl, request.URL.String())
					assert.EqualValues(t, 0, request.HTTPRequest.ContentLength)
					return ic.Response{}, earlyExitErr
				}).Once()

				_, err := scanner.Scan(scanners.Input{})
				assert.ErrorIs(t, earlyExitErr, err)
			})
		})

		t.Run("request with Content-Type header", func(t *testing.T) {
			t.Run("name contains known extension", func(t *testing.T) {
				client.EXPECT().Do(mock.Anything).Return(ic.Response{}, nil).Once()

				client.EXPECT().Do(mock.Anything).RunAndReturn(func(request ic.Request) (ic.Response, error) {
					assert.Equal(t, "application/pdf", request.HTTPRequest.Header.Get("Content-Type"))
					return ic.Response{}, earlyExitErr
				}).Once()

				_, err := scanner.Scan(scanners.Input{Name: "report.pdf"})
				assert.ErrorIs(t, earlyExitErr, err)
			})

			t.Run("name with unknown extension", func(t *testing.T) {
				client.EXPECT().Do(mock.Anything).Return(ic.Response{}, nil).Once()

				client.EXPECT().Do(mock.Anything).RunAndReturn(func(request ic.Request) (ic.Response, error) {
					assert.Equal(t, "application/octet-stream", request.HTTPRequest.Header.Get("Content-Type"))
					return ic.Response{}, earlyExitErr
				}).Once()

				_, err := scanner.Scan(scanners.Input{Name: "report.unknown"})
				assert.ErrorIs(t, earlyExitErr, err)
			})

			t.Run("name without extension", func(t *testing.T) {
				client.EXPECT().Do(mock.Anything).Return(ic.Response{}, nil).Once()

				client.EXPECT().Do(mock.Anything).RunAndReturn(func(request ic.Request) (ic.Response, error) {
					assert.Equal(t, "httpd/unix-directory", request.HTTPRequest.Header.Get("Content-Type"))
					return ic.Response{}, earlyExitErr
				}).Once()

				_, err := scanner.Scan(scanners.Input{Name: "report"})
				assert.ErrorIs(t, earlyExitErr, err)
			})
		})

		t.Run("request with the OPTIONS response preview size ", func(t *testing.T) {
			t.Run("with PreviewBytes set", func(t *testing.T) {
				client.EXPECT().Do(mock.Anything).Return(ic.Response{PreviewBytes: 444}, nil).Once()

				client.EXPECT().Do(mock.Anything).RunAndReturn(func(request ic.Request) (ic.Response, error) {
					assert.Equal(t, 444, request.PreviewBytes)
					return ic.Response{}, earlyExitErr
				}).Once()

				_, err := scanner.Scan(scanners.Input{Body: bytes.NewReader(make([]byte, 888))})
				assert.ErrorIs(t, earlyExitErr, err)
			})

			t.Run("without PreviewBytes set", func(t *testing.T) {
				client.EXPECT().Do(mock.Anything).Return(ic.Response{}, nil).Once()

				client.EXPECT().Do(mock.Anything).RunAndReturn(func(request ic.Request) (ic.Response, error) {
					assert.Equal(t, 0, request.PreviewBytes)
					return ic.Response{}, earlyExitErr
				}).Once()

				_, err := scanner.Scan(scanners.Input{Body: bytes.NewReader(make([]byte, 888))})
				assert.ErrorIs(t, earlyExitErr, err)
			})
		})
	})

	t.Run("request with the OPTIONS response preview size ", func(t *testing.T) {
		t.Run("with PreviewBytes set", func(t *testing.T) {
			client.EXPECT().Do(mock.Anything).Return(ic.Response{PreviewBytes: 444}, nil).Once()

			client.EXPECT().Do(mock.Anything).RunAndReturn(func(request ic.Request) (ic.Response, error) {
				assert.Equal(t, 444, request.PreviewBytes)
				return ic.Response{}, earlyExitErr
			}).Once()

			_, err := scanner.Scan(scanners.Input{Body: bytes.NewReader(make([]byte, 888))})
			assert.ErrorIs(t, earlyExitErr, err)
		})

		t.Run("it handles virus scan results", func(t *testing.T) {
			t.Run("no virus", func(t *testing.T) {
				client.EXPECT().Do(mock.Anything).Return(ic.Response{}, nil).Once()
				client.EXPECT().Do(mock.Anything).Return(ic.Response{}, nil).Once()

				result, err := scanner.Scan(scanners.Input{})
				assert.Nil(t, err)
				assert.False(t, result.Infected)
			})

			// clamav returns an X-Infection-Found header with the threat description
			t.Run("X-Infection-Found header ", func(t *testing.T) {
				client.EXPECT().Do(mock.Anything).Return(ic.Response{}, nil).Once()
				client.EXPECT().Do(mock.Anything).Return(ic.Response{Header: http.Header{"X-Infection-Found": []string{"Threat=bad threat;"}}}, nil).Once()

				result, err := scanner.Scan(scanners.Input{})
				assert.Nil(t, err)
				assert.True(t, result.Infected)
				assert.Equal(t, "bad threat", result.Description)
			})

			// skyhigh returns the information via the content response
			t.Run("X-Infection-Found header", func(t *testing.T) {
				client.EXPECT().Do(mock.Anything).Return(ic.Response{}, nil).Once()
				client.EXPECT().Do(mock.Anything).Return(ic.Response{ContentResponse: &http.Response{StatusCode: http.StatusForbidden, Status: "some status"}}, nil).Once()

				result, err := scanner.Scan(scanners.Input{})
				assert.Nil(t, err)
				assert.True(t, result.Infected)
				assert.Equal(t, "some status", result.Description)

				client.EXPECT().Do(mock.Anything).Return(ic.Response{}, nil).Once()
				client.EXPECT().Do(mock.Anything).Return(ic.Response{ContentResponse: &http.Response{StatusCode: http.StatusOK}}, nil).Once()

				result, err = scanner.Scan(scanners.Input{})
				assert.Nil(t, err)
				assert.False(t, result.Infected)
			})
		})
	})
}
