/*
    Copyright (C) 2022 Tenable, Inc.

	Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

		http://www.apache.org/licenses/LICENSE-2.0

	Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

package vulnerability

import (
	"bytes"
	"crypto/tls"
	"fmt"
	"io"
	"net/http"
	"os"
	"reflect"
	"testing"

	"github.com/tenable/terrascan/pkg/iac-providers/output"
)

var vulnerabilityResponse = `{
	"application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0": {
	  "vulnerabilities": [
		{
		  "id": "CVE-2021-36159",
		  "package": "apk-tools",
		  "version": "2.10.5-r1",
		  "fix_version": "2.10.7-r0",
		  "severity": "Critical",
		  "description": "foo",
		  "vendor_attributes": {
			"CVSS": {
			  "nvd": {
				"V2Score": 6.4,
				"V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:P",
				"V3Score": 9.1,
				"V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H"
			  }
			}
		  }
		}
	  ]
  }
  }`

type mockHarborScanner struct {
	req            *http.Request
	prepareReqErr  error
	data           []byte
	sendRequestErr error
	client         *http.Client
}

func (m mockHarborScanner) prepareRequest(serverURL string) (*http.Request, error) {
	return m.req, m.prepareReqErr
}
func (m mockHarborScanner) sendRequest(caller ServerCaller, req *http.Request) ([]byte, error) {
	return m.data, m.sendRequestErr
}
func (m mockHarborScanner) getClient() *http.Client {
	return m.client
}

// mockClient is the mock client
type mockClient struct {
	res *http.Response
	err error
}

func (m mockClient) Do(req *http.Request) (*http.Response, error) {
	return m.res, m.err
}

func TestHarborScanImage(t *testing.T) {
	type fields struct {
		scanner harborScanner
	}
	type args struct {
		image string
	}
	tests := []struct {
		name       string
		fields     fields
		args       args
		wantResult map[string]interface{}
		wantErr    error
	}{
		{
			name: "error while preparing request",
			args: args{
				image: "test.com/test/terrascan:test",
			},
			wantErr: fmt.Errorf("incomplete authentication details"),
			fields: fields{
				scanner: mockHarborScanner{
					prepareReqErr: fmt.Errorf("incomplete authentication details"),
					client:        &http.Client{},
				},
			},
		},
		{
			name: "error while unmarshaling response",
			args: args{
				image: "test.com/test/terrascan@sha256:86ff209650d222f89449fd2c7090195a6ed00ae3ff0dbebb9d06c21ef576d5e0",
			},
			wantErr: fmt.Errorf("error unmarshaling harbor server response : invalid character 'e' in literal true (expecting 'r')"),
			fields: fields{
				scanner: mockHarborScanner{
					req:    &http.Request{},
					client: &http.Client{},
					data:   []byte("test"),
				},
			},
		},
		{
			name: "error sending request to server",
			args: args{
				image: "test.com/test/terrascan",
			},
			wantErr: fmt.Errorf("error calling harbor server"),
			fields: fields{
				scanner: mockHarborScanner{
					req:            &http.Request{},
					client:         &http.Client{},
					sendRequestErr: fmt.Errorf("error calling harbor server"),
				},
			},
		},
		{
			name: "success response from server",
			args: args{
				image: "test.com/test/terrascan:test",
			},
			wantErr: nil,
			wantResult: map[string]interface{}{
				"application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0": map[string]interface{}{
					"vulnerabilities": []interface{}{map[string]interface{}{
						"description": "foo",
						"fix_version": "2.10.7-r0",
						"id":          "CVE-2021-36159",
						"package":     "apk-tools",
						"severity":    "Critical",
						"vendor_attributes": map[string]interface{}{
							"CVSS": map[string]interface{}{
								"nvd": map[string]interface{}{
									"V2Score":  6.4,
									"V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:P",
									"V3Score":  9.1,
									"V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H"}}},
						"version": "2.10.5-r1"}}}},
			fields: fields{
				scanner: mockHarborScanner{
					req:    &http.Request{},
					client: &http.Client{},
					data:   []byte(vulnerabilityResponse),
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			h := &Harbor{
				scanner: tt.fields.scanner,
			}
			gotResult, err := h.ScanImage(tt.args.image)
			if !reflect.DeepEqual(err, tt.wantErr) {
				t.Errorf("Harbor.ScanImage() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(gotResult, tt.wantResult) {
				t.Errorf("Harbor.ScanImage() = %v, want %v", gotResult, tt.wantResult)
			}
		})
	}
}

func TestGetProjectNameAndRepository(t *testing.T) {
	type args struct {
		name string
	}
	tests := []struct {
		name  string
		args  args
		want  string
		want1 string
	}{
		{
			name: "valid repository",
			args: args{
				name: "terrascan/test",
			},
			want:  "terrascan",
			want1: "test",
		},
		{
			name: "invalid repository",
			args: args{
				name: "test",
			},
			want:  "",
			want1: "",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, got1 := getProjectNameAndRepository(tt.args.name)
			if got != tt.want {
				t.Errorf("getProjectNameAndRepository() got = %v, want %v", got, tt.want)
			}
			if got1 != tt.want1 {
				t.Errorf("getProjectNameAndRepository() got1 = %v, want %v", got1, tt.want1)
			}
		})
	}
}

func TestHarborGetVulnerabilities(t *testing.T) {
	type fields struct {
		scanner harborScanner
	}
	type args struct {
		container output.ContainerDetails
		options   map[string]interface{}
	}
	tests := []struct {
		name                string
		fields              fields
		args                args
		wantVulnerabilities []output.Vulnerability
	}{
		{
			name: "error scanning image",
			args: args{
				container: output.ContainerDetails{
					Name:  "test",
					Image: "test.com/test/terrascan:test",
				},
			},
			fields: fields{
				scanner: mockHarborScanner{
					prepareReqErr: fmt.Errorf("incomplete authentication details"),
					client:        &http.Client{},
				},
			},
		},
		{
			name: "success response from image scan",
			args: args{
				container: output.ContainerDetails{
					Name:  "test",
					Image: "test.com/test/terrascan:test",
				},
			},
			wantVulnerabilities: []output.Vulnerability{
				{
					VulnerabilityID:  "CVE-2021-36159",
					PkgName:          "apk-tools",
					InstalledVersion: "2.10.5-r1",
					FixedVersion:     "2.10.7-r0",
					PrimaryURL:       "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-36159",
					Description:      "foo",
					Severity:         "Critical",
					CVSS: output.VendorCVSS{
						"nvd": output.CVSS{
							V2Vector: "AV:N/AC:L/Au:N/C:P/I:N/A:P",
							V3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H",
							V2Score:  6.4,
							V3Score:  9.1,
						}},
				}},
			fields: fields{
				scanner: mockHarborScanner{
					req:    &http.Request{},
					client: &http.Client{},
					data:   []byte(vulnerabilityResponse),
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			h := &Harbor{
				scanner: tt.fields.scanner,
			}
			if gotVulnerabilities := h.getVulnerabilities(tt.args.container, tt.args.options); !reflect.DeepEqual(gotVulnerabilities, tt.wantVulnerabilities) {
				t.Errorf("Harbor.getVulnerabilities() = %v, want %v", gotVulnerabilities, tt.wantVulnerabilities)
			}
		})
	}
}

func TestPrepareRequest(t *testing.T) {
	type args struct {
		serverURL string
	}
	tests := []struct {
		name        string
		h           hscanner
		args        args
		want        *http.Request
		wantErr     bool
		setEnv      bool
		checkHeader bool
	}{
		{
			name: "incomplete details",
			args: args{
				serverURL: "test.com",
			},
			wantErr: true,
		},
		{
			name: "get request details",
			args: args{
				serverURL: "test.com",
			},
			setEnv:  true,
			wantErr: false,

			checkHeader: true,
			want: &http.Request{
				Header: http.Header{"Accept": []string{"application/json"}, "Authorization": []string{"Basic dmFsdWU6dmFsdWU="},
					"X-Accept-Vulnerabilities": []string{"application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0"}},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			h := hscanner{}
			if tt.setEnv {
				os.Setenv(registryUsername, "value")
				defer os.Unsetenv(registryUsername)
				os.Setenv(registryPassword, "value")
				defer os.Unsetenv(registryPassword)
			} else {
				os.Unsetenv(registryPassword)
				os.Unsetenv(registryUsername)
			}
			got, err := h.prepareRequest(tt.args.serverURL)
			if (err != nil) != tt.wantErr {
				t.Errorf("hscanner.prepareRequest() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if tt.checkHeader && !reflect.DeepEqual(got.Header, tt.want.Header) {
				t.Errorf("hscanner.prepareRequest() got = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestSendRequest(t *testing.T) {
	type args struct {
		caller ServerCaller
		req    *http.Request
	}
	tests := []struct {
		name    string
		h       hscanner
		args    args
		want    []byte
		wantErr error
	}{
		{
			name: "error sending request",
			args: args{
				caller: mockClient{
					err: fmt.Errorf("error sending request"),
				},
			},
			wantErr: fmt.Errorf("error calling harbor server: error sending request"),
		},
		{
			name: "success sending request but unauthorized error in response",
			args: args{
				caller: mockClient{
					res: &http.Response{
						StatusCode: 401,
						Body:       io.NopCloser(bytes.NewReader([]byte(`error:["unauthorized"]`))),
					},
				},
			},
			wantErr: fmt.Errorf(`error:["unauthorized"]`),
		},
		{
			name: "success sending request",
			args: args{
				caller: mockClient{
					res: &http.Response{
						StatusCode: 200,
						Body:       io.NopCloser(bytes.NewReader([]byte(vulnerabilityResponse))),
					},
				},
			},
			want: []byte(vulnerabilityResponse),
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			h := hscanner{}
			got, err := h.sendRequest(tt.args.caller, tt.args.req)
			if !reflect.DeepEqual(err, tt.wantErr) {
				t.Errorf("hscanner.sendRequest() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("hscanner.sendRequest() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestGetClient(t *testing.T) {
	tests := []struct {
		name   string
		h      hscanner
		want   *http.Client
		setEnv bool
	}{
		{
			name: "client without cert",
			want: &http.Client{
				Transport: &http.Transport{
					TLSClientConfig: &tls.Config{},
				}},
		},
		{
			name:   "client with InsecureSkipVerify",
			setEnv: true,
			want: &http.Client{
				Transport: &http.Transport{
					TLSClientConfig: &tls.Config{
						InsecureSkipVerify: true,
					},
				}},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			h := hscanner{}
			if tt.setEnv {
				os.Setenv(skipTLSVerify, "true")
				defer os.Unsetenv(skipTLSVerify)
			} else {
				os.Unsetenv(skipTLSVerify)
			}
			if got := h.getClient(); !reflect.DeepEqual(got, tt.want) {
				t.Errorf("hscanner.getClient() = %v, want %v", got, tt.want)
			}
		})
	}
}
