package postman

import (
	"fmt"
	"reflect"
	"sort"
	"strings"
	"testing"
	"time"

	"github.com/go-errors/errors"
	"github.com/stretchr/testify/assert"

	"github.com/trufflesecurity/trufflehog/v3/pkg/context"
	"gopkg.in/h2non/gock.v1"

	"github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb"
	"github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb"
	"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
	"google.golang.org/protobuf/types/known/anypb"
)

func createTestSource(src *sourcespb.Postman) (*Source, *anypb.Any) {
	s := &Source{
		metrics: newMetrics("Test Source"),
	}
	conn, err := anypb.New(src)
	if err != nil {
		panic(err)
	}
	return s, conn
}

func TestSource_Init(t *testing.T) {
	s, conn := createTestSource(&sourcespb.Postman{
		Credential: &sourcespb.Postman_Token{
			Token: "super secret token",
		},
	})
	s.DetectorKeywords = map[string]struct{}{
		"keyword1": {},
		"keyword2": {},
	}

	err := s.Init(context.Background(), "test - postman", 0, 1, false, conn, 10)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	expectedKeywords := map[string]struct{}{
		"keyword1": {},
		"keyword2": {},
	}
	if !reflect.DeepEqual(s.DetectorKeywords, expectedKeywords) {
		t.Errorf("expected detector keywords: %v, got: %v", expectedKeywords, s.DetectorKeywords)
	}
}

func TestSource_ScanCollection(t *testing.T) {
	ctx := context.Background()
	s := &Source{
		DetectorKeywords: map[string]struct{}{
			"keyword1": {},
		},
		keywords: map[string]struct{}{
			"keyword1": {},
		},
	}

	testCases := []struct {
		name           string
		collection     Collection
		expectedChunks []string
	}{
		{
			name: "GET request with URL",
			collection: Collection{
				Info: Info{
					PostmanID: "col1",
					Name:      "Test Collection",
				},
				Items: []Item{
					{
						Name: "Request 1",
						Request: Request{
							URL: URL{
								Protocol: "https",
								Host:     []string{"example.com"},
								Path:     []string{"api", "endpoint"},
								Raw:      "https://example.com/api/endpoint",
							},
							Method: "GET",
						},
					},
				},
			},
			expectedChunks: []string{
				"keyword1:https://example.com/api/endpoint\n",
			},
		},
		{
			name: "POST request with URL and auth",
			collection: Collection{
				Info: Info{
					PostmanID: "col2",
					Name:      "Test Collection",
				},
				Items: []Item{
					{
						Name: "Request 2",
						Request: Request{
							URL: URL{
								Protocol: "https",
								Host:     []string{"example.com"},
								Path:     []string{"api", "endpoint"},
								Raw:      "https://example.com/api/endpoint",
							},
							Method: "POST",
							Auth: Auth{
								Type: "bearer",
								Bearer: []KeyValue{
									{
										Key:   "token",
										Value: "abcdef123456",
									},
								},
							},
						},
					},
				},
			},
			expectedChunks: []string{
				"keyword1:https://example.com/api/endpoint\n",
				"keyword1:token:abcdef123456 \n",
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			chunksChan := make(chan *sources.Chunk, len(tc.expectedChunks))
			metadata := Metadata{
				CollectionInfo: tc.collection.Info,
			}

			go s.scanCollection(ctx, chunksChan, metadata, tc.collection)

			for _, expectedData := range tc.expectedChunks {
				chunk := <-chunksChan

				// can't guarantee order of keywords in chunk data
				// so we need to compare the data after sorting
				got := strings.Split(strings.TrimSpace(string(chunk.Data)), "\n")
				expected := strings.Split(strings.TrimSpace(expectedData), "\n")
				sort.Strings(got)
				sort.Strings(expected)

				if !reflect.DeepEqual(got, expected) {
					t.Errorf("expected chunk data from collection: \n%sgot: \n%s", expectedData, chunk.Data)
				}
			}
		})
	}
}

func TestSource_ScanCollectionWithFolders(t *testing.T) {
	ctx := context.Background()
	s := &Source{
		DetectorKeywords: map[string]struct{}{
			"keyword1": {},
		},
		keywords: map[string]struct{}{
			"keyword1": {},
		},
	}
	testCollection := Collection{
		Info: Info{
			PostmanID: "col1",
			Name:      "Test Collection with Folders",
		},
		Items: []Item{
			{
				Name: "Folder 1",
				Items: []Item{
					{
						Uid:  "1",
						Name: "Request 1",
						Request: Request{
							URL: URL{
								Protocol: "https",
								Host:     []string{"example.com"},
								Path:     []string{"api", "endpoint"},
								Raw:      "https://example.com/api/endpoint",
							},
							Method: "GET",
						},
					},
					{
						Uid:  "2",
						Name: "Folder 2",
						Items: []Item{
							{
								Uid:  "3",
								Name: "Request 2",
								Request: Request{
									URL: URL{
										Protocol: "https",
										Host:     []string{"test.com"},
										Path:     []string{"api", "endpoint"},
										Raw:      "https://test.com/api/endpoint",
									},
									Method: "POST",
									Auth: Auth{
										Type: "bearer",
										Bearer: []KeyValue{
											{
												Key:   "token",
												Value: "abcdef123456",
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
	}

	expectedChunks := []string{
		"keyword1:https://example.com/api/endpoint\n",
		"keyword1:https://test.com/api/endpoint\n",
		"keyword1:token:abcdef123456\n",
	}

	chunksChan := make(chan *sources.Chunk, len(expectedChunks))
	metadata := Metadata{
		CollectionInfo: testCollection.Info,
	}

	go s.scanCollection(ctx, chunksChan, metadata, testCollection)

	for _, expectedData := range expectedChunks {
		chunk := <-chunksChan
		got := strings.Split(strings.TrimSpace(string(chunk.Data)), "\n")
		expected := strings.Split(strings.TrimSpace(expectedData), "\n")
		sort.Strings(got)
		sort.Strings(expected)

		if !reflect.DeepEqual(got, expected) {
			t.Errorf("expected chunk data from collection: \n%sgot: \n%s", expectedData, chunk.Data)
		}
	}
}

func TestSource_ScanSingleItem(t *testing.T) {
	ctx := context.Background()
	s := &Source{
		DetectorKeywords: map[string]struct{}{
			"keyword1": {},
		},
		keywords: map[string]struct{}{
			"keyword1": {},
		},
	}
	testItem := Item{
		Name: "Request 1",
		Request: Request{
			URL: URL{
				Protocol: "https",
				Host:     []string{"example.com"},
				Path:     []string{"api", "endpoint"},
				Raw:      "https://example.com/api/endpoint",
			},
			Method: "GET",
		},
	}
	expectedChunks := []string{
		"keyword1:https://example.com/api/endpoint\n",
	}
	chunksChan := make(chan *sources.Chunk, len(expectedChunks))
	metadata := Metadata{}
	s.scanItem(ctx, chunksChan, Collection{}, metadata, testItem, "", map[string]struct{}{})
	for _, expectedData := range expectedChunks {
		chunk := <-chunksChan
		got := strings.Split(strings.TrimSpace(string(chunk.Data)), "\n")
		expected := strings.Split(strings.TrimSpace(expectedData), "\n")
		sort.Strings(got)
		sort.Strings(expected)
		if !reflect.DeepEqual(got, expected) {
			t.Errorf("expected chunk data from collection: \n%sgot: \n%s", expectedData, chunk.Data)
		}
	}
}

func TestSource_ScanNestedItems(t *testing.T) {
	ctx := context.Background()
	s := &Source{
		DetectorKeywords: map[string]struct{}{
			"keyword1": {},
		},
		keywords: map[string]struct{}{
			"keyword1": {},
		},
	}
	testItem := Item{
		Name: "Folder 1",
		Items: []Item{
			{
				Name: "Request 1",
				Uid:  "1",
				Request: Request{
					URL: URL{
						Protocol: "https",
						Host:     []string{"example.com"},
						Path:     []string{"api", "endpoint"},
						Raw:      "https://example.com/api/endpoint",
					},
					Method: "GET",
				},
			},
			{
				Name: "Folder 2",
				Uid:  "2",
				Items: []Item{
					{
						Uid:  "3",
						Name: "Request 2",
						Request: Request{
							URL: URL{
								Protocol: "https",
								Host:     []string{"test.com"},
								Path:     []string{"api", "endpoint"},
								Raw:      "https://test.com/api/endpoint",
							},
							Method: "POST",
						},
					},
				},
			},
		},
	}

	expectedChunks := []string{
		"keyword1:https://example.com/api/endpoint\n",
		"keyword1:https://test.com/api/endpoint\n",
	}

	chunksChan := make(chan *sources.Chunk, len(expectedChunks))
	metadata := Metadata{}
	s.scanItem(ctx, chunksChan, Collection{}, metadata, testItem, "", map[string]struct{}{})
	for _, expectedData := range expectedChunks {
		chunk := <-chunksChan
		got := strings.Split(strings.TrimSpace(string(chunk.Data)), "\n")
		expected := strings.Split(strings.TrimSpace(expectedData), "\n")
		sort.Strings(got)
		sort.Strings(expected)
		if !reflect.DeepEqual(got, expected) {
			t.Errorf("expected chunk data from collection: \n%sgot: \n%s", expectedData, chunk.Data)
		}
	}
}

func TestSource_ScanVariableData(t *testing.T) {
	ctx := context.Background()
	s := &Source{
		DetectorKeywords: map[string]struct{}{
			"keyword1": {},
		},
		keywords: map[string]struct{}{
			"keyword1": {},
		},
		sub: NewSubstitution(),
	}

	testCases := []struct {
		name           string
		metadata       Metadata
		variableData   VariableData
		expectedChunks []string
	}{
		{
			name: "Single variable",
			metadata: Metadata{
				CollectionInfo: Info{
					PostmanID: "col1",
					Name:      "Test Collection",
				},
			},
			variableData: VariableData{
				KeyValues: []KeyValue{
					{
						Key:   "var1",
						Value: "value1",
					},
				},
			},
			expectedChunks: []string{
				"keyword1:value1\n",
			},
		},
		{
			name: "Multiple variables",
			metadata: Metadata{
				CollectionInfo: Info{
					PostmanID: "col2",
					Name:      "Test Collection",
				},
			},
			variableData: VariableData{
				KeyValues: []KeyValue{
					{
						Key:   "var1",
						Value: "value1",
					},
					{
						Key:   "var2",
						Value: "value2",
					},
				},
			},
			expectedChunks: []string{
				"keyword1:value1\nkeyword1:value2\n",
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			chunksChan := make(chan *sources.Chunk, len(tc.expectedChunks))

			s.scanVariableData(ctx, chunksChan, tc.metadata, tc.variableData)

			for _, expectedData := range tc.expectedChunks {
				chunk := <-chunksChan
				got := strings.Split(strings.TrimSpace(string(chunk.Data)), "\n")
				expected := strings.Split(strings.TrimSpace(expectedData), "\n")
				sort.Strings(got)
				sort.Strings(expected)

				if !reflect.DeepEqual(got, expected) {
					t.Errorf("expected chunk data from collection: \n%sgot: \n%s", expectedData, chunk.Data)
				}
			}
		})
	}
}

func TestSource_ScanEnumerateWorkspaceRateLimit(t *testing.T) {
	defer gock.Off()
	// Mock the API response for workspaces
	numWorkspaces := 3
	workspaceBodyString := `{"workspaces":[`
	for i := 0; i < numWorkspaces; i++ {
		workspaceBodyString += fmt.Sprintf(`{"id": "%d", "name": "workspace-%d", "type": "personal", "visibility": "personal", "createdBy": "1234"}`, i, i)
		if i == numWorkspaces-1 {
			workspaceBodyString += `]}`
		} else {
			workspaceBodyString += `,`
		}
	}
	gock.New("https://api.getpostman.com").
		Get("/workspaces").
		Reply(200).
		BodyString(workspaceBodyString)
	// Mock the API response for each individual workspace
	for i := 0; i < numWorkspaces; i++ {
		gock.New("https://api.getpostman.com").
			Get(fmt.Sprintf("/workspaces/%d", i)).
			Reply(200).
			BodyString(fmt.Sprintf(`{"workspace":{"id":"%d","name":"workspace-%d","type":"personal","description":"Test workspace number %d",
			"visibility":"personal","createdBy":"1234","updatedBy":"1234","createdAt":"2024-12-12T23:32:27.000Z","updatedAt":"2024-12-12T23:33:01.000Z",
			"collections":[{"id":"abc%d","name":"test-collection-1","uid":"1234-abc%d"},{"id":"def%d","name":"test-collection-2","uid":"1234-def%d"}],
			"environments":[{"id":"ghi%d","name":"test-environment-1","uid":"1234-ghi%d"},{"id":"jkl%d","name":"test-environment-2","uid":"1234-jkl%d"}]}}`, i, i, i, i, i, i, i, i, i, i, i))
	}

	ctx := context.Background()
	s, conn := createTestSource(&sourcespb.Postman{
		Credential: &sourcespb.Postman_Token{
			Token: "super-secret-token",
		},
	})
	err := s.Init(ctx, "test - postman", 0, 1, false, conn, 1)
	if err != nil {
		t.Fatalf("init error: %v", err)
	}
	gock.InterceptClient(s.client.HTTPClient)
	defer gock.RestoreClient(s.client.HTTPClient)

	start := time.Now()
	_, err = s.client.EnumerateWorkspaces(ctx)
	if err != nil {
		t.Fatalf("enumeration error: %v", err)
	}
	elapsed := time.Since(start)
	// With <numWorkspaces> requests at 1 per second rate limit,
	// elapsed time should be at least <numWorkspaces - 1> seconds
	if elapsed < time.Duration(numWorkspaces-1)*time.Second {
		t.Errorf("Rate limiting not working as expected. Elapsed time: %v seconds, expected at least %d seconds", elapsed.Seconds(), numWorkspaces-1)
	}
}

func TestSource_ScanGeneralRateLimit(t *testing.T) {
	defer gock.Off()
	// Mock the API response for a specific environment id
	gock.New("https://api.getpostman.com").
		Get("environments/abc").
		Persist().
		Reply(200).
		BodyString(`{"environment":{"uid":"1234-abc","id":"abc","name":"test-environment","owner":"1234","createdAt":"2025-02-13T23:17:36.000Z",
		"updatedAt":"2025-02-13T23:18:14.000Z","values":[{"key":"test-key","value":"a-secret-value","enabled":true,"type":"default"}],"isPublic":false}}`)
	ctx := context.Background()
	s, conn := createTestSource(&sourcespb.Postman{
		Credential: &sourcespb.Postman_Token{
			Token: "super-secret-token",
		},
	})
	err := s.Init(ctx, "test - postman", 0, 1, false, conn, 1)
	if err != nil {
		t.Fatalf("init error: %v", err)
	}
	gock.InterceptClient(s.client.HTTPClient)
	defer gock.RestoreClient(s.client.HTTPClient)

	numRequests := 3
	start := time.Now()
	for i := 0; i < numRequests; i++ {
		_, err = s.client.GetEnvironmentVariables(ctx, "abc")
		if err != nil {
			t.Fatalf("get environment variables error: %v", err)
		}
	}
	elapsed := time.Since(start)
	// With number of requests at 5 per second rate limit,
	// elapsed time should be at least <(numRequests - 1)/5> seconds
	if elapsed < time.Duration((numRequests-1)/5)*time.Second {
		t.Errorf("Rate limiting not working as expected. Elapsed time: %v seconds, expected at least %v seconds", elapsed.Seconds(), (float64(numRequests)-1)/5)
	}
}

func TestSource_BadPostmanCollectionApiResponseDoesntEndScan(t *testing.T) {
	// The goal here is to make sure that, if we get a bad ID for a collection (or some other request issue) and the
	// Postman API gives us a non 200 responses, it doesn't stop the whole scan.  To do that we're going to  have it get
	// a set of 3 collections from /collections/ and then mock all but the last request as bad in some way.  Then we'll
	// check that the third one was properly requested
	defer gock.Off()

	// IDs we'll use later
	userId := "12345678"
	workspaceId := "1f0df51a-8658-4ee8-a2a1-d2567dfa09a9"
	id1CollectionBadResponse := "dac5eac9-148d-a32e-b76b-3edee9da28f7"
	id2CollectionBadId := "12ece9e1-2abf-4edc-8e34-de66e74114d2"
	id3CollectionGood := "f695cab7-6878-eb55-7943-ad88e1ccfd65"
	uid1CollectionBadResponse := fmt.Sprintf("%s-%s", userId, id1CollectionBadResponse)
	uid2CollectionBadId := fmt.Sprintf("%s-%s", userId, id2CollectionBadId)
	uid3CollectionGood := fmt.Sprintf("%s-%s", userId, id3CollectionGood)

	// Mock the workspace list response
	gock.New("https://api.getpostman.com").
		Get("/workspaces").
		Reply(200).
		JSON(map[string]interface{}{
			"workspaces": []map[string]interface{}{
				{
					"id":         workspaceId,
					"name":       "My Workspace",
					"createdBy":  userId,
					"type":       "personal",
					"visibility": "personal",
				},
			},
		})
	// Mock the workspace details response
	gock.New("https://api.getpostman.com").
		Get(fmt.Sprintf("/workspaces/%s", workspaceId)).
		Reply(200).
		JSON(map[string]interface{}{
			"workspace": map[string]interface{}{
				"id":          workspaceId,
				"name":        "Team Workspace",
				"type":        "team",
				"description": "This is a team workspace.",
				"visibility":  "team",
				"createdBy":   userId,
				"updatedBy":   userId,
				"createdAt":   "2022-07-06T16:18:32.000Z",
				"updatedAt":   "2022-07-06T20:55:13.000Z",
				"collections": []map[string]interface{}{
					{
						"id":   id1CollectionBadResponse,
						"name": "Test Collection",
						"uid":  uid1CollectionBadResponse,
					},
					{
						"id":   id2CollectionBadId,
						"name": "Test Collection2",
						"uid":  uid2CollectionBadId,
					},
					{
						"id":   id3CollectionGood,
						"name": "Test Collection3",
						"uid":  uid3CollectionGood,
					},
				},
				"environments": []map[string]interface{}{},
				"mocks":        []map[string]interface{}{},
				"monitors":     []map[string]interface{}{},
				"apis":         []map[string]interface{}{},
			},
		})

	// Make a call for the first colection respond with a malformed response
	gock.New("https://api.getpostman.com").
		Get(fmt.Sprintf("/collections/%s", uid1CollectionBadResponse)).
		Reply(200).
		BodyString("INTENTIONALLY MALFORMED RESPONSE HERE")
	// Make a call for the second collection respond not found
	gock.New("https://api.getpostman.com").
		Get(fmt.Sprintf("/collections/%s", uid2CollectionBadId)).
		Reply(404).
		JSON(map[string]interface{}{
			"error": map[string]string{
				"name":    "instanceNotFoundError",
				"message": "We could not find the collection you are looking for",
			},
		})
	// Make a call for the third workspace succeed
	gock.New("https://api.getpostman.com").
		Get(fmt.Sprintf("/collections/%s", uid3CollectionGood)).
		Reply(200).
		JSON(map[string]interface{}{
			"collection": map[string]interface{}{
				"info": map[string]interface{}{
					"_postman_id":   id3CollectionGood,
					"name":          "Test Collection",
					"description":   "This is a test.",
					"schema":        "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
					"updatedAt":     "2023-10-09T18:34:58.000Z",
					"createdAt":     "2023-10-09T18:34:58.000Z",
					"lastUpdatedBy": userId,
					"uid":           uid3CollectionGood,
				},
				"item": []interface{}{},
				"auth": map[string]interface{}{
					"type":   "apikey",
					"apikey": []interface{}{},
				},
				"event":    []interface{}{},
				"variable": []interface{}{},
			},
		})

	// Set up the source and inject the mocks
	ctx := context.Background()
	s, conn := createTestSource(&sourcespb.Postman{
		Credential: &sourcespb.Postman_Token{
			Token: "super-secret-token",
		},
	})
	err := s.Init(ctx, "test - postman", 0, 1, false, conn, 1)
	if err != nil {
		t.Fatalf("init error: %v", err)
	}
	gock.InterceptClient(s.client.HTTPClient)
	defer gock.RestoreClient(s.client.HTTPClient)

	// Do the thing
	workspaces, _ := s.client.EnumerateWorkspaces(ctx)
	_ = s.scanWorkspace(ctx, make(chan *sources.Chunk), workspaces[0])

	// If all the calls were made, then we know the one bad request didn't cause explosions
	assert.True(t, gock.IsDone())

}

func TestSource_BadPostmanWorkspaceApiResponseDoesntEndScan(t *testing.T) {
	// The goal here is to make sure that, if we get a bad ID (or other issue) for a workspace and the Postman API
	// gives us a non 200 responses, it doesn't stop the whole scan.  To do that we're going to  have it get a set
	// of 3 workspaces from /workspaces/ and then mock all but the last as a bad request.  Then we'll check that the
	// third one was properly requested.
	defer gock.Off()

	// We'll use the IDs later in a couple of places
	id1WorkspaceBadRequest := "1f0df51a-8658-4ee8-a2a1-d2567dfa09a9"
	id2WorkspaceBadId := "a0f46158-1529-11ee-be56-0242ac120002"
	id3WorkspaceGood := "f8801e9e-03a4-4c7b-b31e-5db5cd771696"

	// Mock the workspace list response.  This gives EnumerateWorkspaces what it needs
	// to make calls for the individual workspaces details
	gock.New("https://api.getpostman.com").
		Get("/workspaces").
		Reply(200).
		JSON(map[string]interface{}{
			"workspaces": []map[string]interface{}{
				{
					"id":         id1WorkspaceBadRequest,
					"name":       "My Workspace",
					"createdBy":  "12345678",
					"type":       "personal",
					"visibility": "personal",
				},
				{
					"id":         id2WorkspaceBadId,
					"name":       "Private Workspace",
					"createdBy":  "12345678",
					"type":       "team",
					"visibility": "private",
				},
				{
					"id":         id3WorkspaceGood,
					"name":       "Team Workspace",
					"createdBy":  "12345678",
					"type":       "team",
					"visibility": "team",
				},
			},
		})

	// Make a call for the first workspace respond with a malformed response
	gock.New("https://api.getpostman.com").
		Get(fmt.Sprintf("/workspaces/%s", id1WorkspaceBadRequest)).
		Reply(200).
		BodyString("INTENTIONALLY MALFORMED RESPONSE BODY")
	// Make a call for the second workspace respond not found
	gock.New("https://api.getpostman.com").
		Get(fmt.Sprintf("/workspaces/%s", id2WorkspaceBadId)).
		Reply(404).
		JSON(map[string]interface{}{
			"error": map[string]interface{}{
				"name":       "workspaceNotFoundError",
				"mesage":     "workspace not found",
				"statusCode": 404,
			},
		})
	// Make a call for the third workspace succeed
	gock.New("https://api.getpostman.com").
		Get(fmt.Sprintf("/workspaces/%s", id3WorkspaceGood)).
		Reply(200).
		JSON(map[string]interface{}{
			"workspace": map[string]interface{}{
				"id":           id3WorkspaceGood,
				"name":         "Team Workspace",
				"type":         "team",
				"description":  "This is a team workspace.",
				"visibility":   "team",
				"createdBy":    "12345678",
				"updatedBy":    "12345678",
				"createdAt":    "2022-07-06T16:18:32.000Z",
				"updatedAt":    "2022-07-06T20:55:13.000Z",
				"collections":  []map[string]interface{}{},
				"environments": []map[string]interface{}{},
				"mocks":        []map[string]interface{}{},
				"monitors":     []map[string]interface{}{},
				"apis":         []map[string]interface{}{},
			},
		})

	// Set up the source and inject the mocks
	ctx := context.Background()
	s, conn := createTestSource(&sourcespb.Postman{
		Credential: &sourcespb.Postman_Token{
			Token: "super-secret-token",
		},
	})
	err := s.Init(ctx, "test - postman", 0, 1, false, conn, 1)
	if err != nil {
		t.Fatalf("init error: %v", err)
	}
	gock.InterceptClient(s.client.HTTPClient)
	defer gock.RestoreClient(s.client.HTTPClient)

	// Do the thing
	_, _ = s.client.EnumerateWorkspaces(ctx)

	// If all the calls were made, then we know the one bad request didn't cause explosions
	assert.True(t, gock.IsDone())

}

func TestSource_UnmarshalMultipleHeaderTypes(t *testing.T) {
	defer gock.Off()
	// Mock a collection with request and response headers of KeyValue type
	gock.New("https://api.getpostman.com").
		Get("/collections/1234-abc1").
		Reply(200).
		BodyString(`{"collection":{"info":{"_postman_id":"abc1","name":"test-collection-1","schema":"https://schema.postman.com/json/collection/v2.1.0/collection.json",
	 	"updatedAt":"2025-03-21T17:39:25.000Z","createdAt":"2025-03-21T17:37:13.000Z","lastUpdatedBy":"1234","uid":"1234-abc1"},
	 	"item":[{"name":"echo","id":"req-ues-t1","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":[{"key":"Date","value":"Fri, 21 Mar 2025 17:38:58 GMT"}]},
	 	"response":[{"id":"res-pon-se1","name":"echo-response","originalRequest":{"method":"GET","header":[{"key":"Date","value":"Fri, 21 Mar 2025 17:38:58 GMT"}],
	 	"url":{"raw":"postman-echo.com/get","host":["postman-echo","com"],"path":["get"]}},"status":"OK","code":200,"_postman_previewlanguage":"json",
	 	"header":[{"key":"Date","value":"Fri, 21 Mar 2025 17:38:58 GMT"},{"key":"Content-Type","value":"application/json; charset=utf-8"},{"key":"Content-Length","value":"508"},
	 	{"key":"Connection","value":"keep-alive"},{"key":"Server","value":"nginx"},{"key":"ETag","value":"random-string"},
		{"key":"set-cookie","value":"sails.sid=long-string; Path=/; HttpOnly"}],"cookie":[], "responseTime":null,"body":"{response-body}","uid":"1234-res-pon-se1"}],"uid":"1234-req-ues-t1"}]}}`)
	// Mock a collection with request and response headers of string type
	gock.New("https://api.getpostman.com").
		Get("/collections/1234-def1").
		Reply(200).
		BodyString(`{"collection":{"info":{"_postman_id":"abc1","name":"test-collection-1","schema":"https://schema.postman.com/json/collection/v2.1.0/collection.json",
	 	"updatedAt":"2025-03-21T17:39:25.000Z","createdAt":"2025-03-21T17:37:13.000Z","lastUpdatedBy":"1234","uid":"1234-def1"},
	 	"item":[{"name":"echo","id":"req-ues-t1","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":["request-header-string"]},
	 	"response":[{"id":"res-pon-se1","name":"echo-response","originalRequest":{"method":"GET","header":["request-header-string"],
	 	"url":{"raw":"postman-echo.com/get","host":["postman-echo","com"],"path":["get"]}},"status":"OK","code":200,"_postman_previewlanguage":"json",
	 	"header":["response-header-string"],"cookie":[], "responseTime":null,"body":"{response-body}","uid":"1234-res-pon-se1"}],"uid":"1234-req-ues-t1"}]}}`)

	ctx := context.Background()
	s, conn := createTestSource(&sourcespb.Postman{
		Credential: &sourcespb.Postman_Token{
			Token: "super-secret-token",
		},
	})
	err := s.Init(ctx, "test - postman", 0, 1, false, conn, 1)
	if err != nil {
		t.Fatalf("init error: %v", err)
	}
	gock.InterceptClient(s.client.HTTPClient)
	defer gock.RestoreClient(s.client.HTTPClient)

	collectionIds := []string{"1234-abc1", "1234-def1"}
	for _, collectionId := range collectionIds {
		_, err := s.client.GetCollection(ctx, collectionId)
		if err != nil {
			t.Fatalf("failed to get collection: %v", err)
		}
	}
}

// The purpose of the TestSource_HeadersScanning test is to check that at least one of the fields HeaderKeyValue or HeaderString are non-null after unmarshalling and that chunks can
// be generated from them.
func TestSource_HeadersScanning(t *testing.T) {
	defer gock.Off()
	// Mock a collection with request and response headers of KeyValue type
	gock.New("https://api.getpostman.com").
		Get("/collections/1234-abc1").
		Reply(200).
		BodyString(`{"collection":{"info":{"_postman_id":"abc1","name":"test-collection-1","schema":"https://schema.postman.com/json/collection/v2.1.0/collection.json",
        "updatedAt":"2025-03-21T17:39:25.000Z","createdAt":"2025-03-21T17:37:13.000Z","lastUpdatedBy":"1234","uid":"1234-abc1"},
        "item":[{"name":"echo","id":"req-ues-t1", "request":{"method":"GET","header":[{"key":"token","value":"keyword1"}]},
        "response":[{"id":"res-pon-se1","name":"echo-response","originalRequest":{"method":"GET","header":[{"key":"token","value":"keyword1"}]},
        "header":[{"key":"token","value":"keyword1"}]}],"uid":"1234-req-ues-t1"}]}}`)
	// Mock a collection with request and response headers of string type
	gock.New("https://api.getpostman.com").
		Get("/collections/1234-def1").
		Reply(200).
		BodyString(`{"collection":{"info":{"_postman_id":"abc1","name":"test-collection-1","schema":"https://schema.postman.com/json/collection/v2.1.0/collection.json",
        "updatedAt":"2025-03-21T17:39:25.000Z","createdAt":"2025-03-21T17:37:13.000Z","lastUpdatedBy":"1234","uid":"1234-def1"},
        "item":[{"name":"echo","id":"req-ues-t1","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":["keyword1-request-header-string"]},
        "response":[{"id":"res-pon-se1","name":"echo-response","originalRequest":{"method":"GET","header":["keyword1-request-header-string"]},
        "header":["keyword1-response-header-string"]}],"uid":"1234-req-ues-t1"}]}}`)

	ctx := context.Background()
	s, conn := createTestSource(&sourcespb.Postman{
		Credential: &sourcespb.Postman_Token{
			Token: "super-secret-token",
		},
	})

	// Add detector keywords to trigger chunk generation
	s.DetectorKeywords = map[string]struct{}{
		"keyword1": {},
	}
	s.keywords = map[string]struct{}{
		"keyword1": {},
	}

	err := s.Init(ctx, "test - postman", 0, 1, false, conn, 1)
	if err != nil {
		t.Fatalf("init error: %v", err)
	}
	gock.InterceptClient(s.client.HTTPClient)
	defer gock.RestoreClient(s.client.HTTPClient)

	chunksChan := make(chan *sources.Chunk, 10)
	collectionIds := []string{"1234-abc1", "1234-def1"}

	for _, collectionId := range collectionIds {
		collection, err := s.client.GetCollection(ctx, collectionId)
		if err != nil {
			t.Fatalf("failed to get collection: %v", err)
		}
		s.scanCollection(ctx, chunksChan, Metadata{CollectionInfo: collection.Info}, collection)
	}

	close(chunksChan)
	chunksReceived := len(chunksChan)

	if chunksReceived == 0 {
		t.Errorf("No chunks were generated from the mock data")
	} else {
		t.Logf("Generated %d chunks from the mock data", chunksReceived)
	}
}

func TestSource_ResponseID(t *testing.T) {
	ctx := context.Background()

	s := &Source{
		DetectorKeywords: map[string]struct{}{
			"keyword1": {},
		},
		keywords: map[string]struct{}{
			"keyword1": {},
		},
	}

	chunksChan := make(chan *sources.Chunk, 1)

	response := Response{
		Uid:  "response-1234",
		Name: "Test Response",
		Body: "This is a test response body containing keyword1.",
	}

	go func() {
		s.scanHTTPResponse(ctx, chunksChan, Metadata{}, response)
		close(chunksChan)
	}()

	var gotChunk *sources.Chunk
	for chunk := range chunksChan {
		gotChunk = chunk
	}

	postmanMetadata, ok := gotChunk.SourceMetadata.Data.(*source_metadatapb.MetaData_Postman)
	if !ok {
		t.Fatalf("expected Postman metadata, got: %T", gotChunk.SourceMetadata.Data)
	}

	if postmanMetadata.Postman.ResponseId != response.Uid {
		t.Errorf("expected response ID: %s, got: %s", response.Uid, postmanMetadata.Postman.ResponseId)
	}
	if postmanMetadata.Postman.ResponseName != response.Name {
		t.Errorf("expected response Name: %s, got: %s", response.Name, postmanMetadata.Postman.ResponseName)
	}

}

func TestSource_AbortScanRateLimitCrossedThreshold(t *testing.T) {
	defer gock.Off()
	// Mock the API response for getting collection
	gock.New("https://api.getpostman.com").
		Get("/collections/1234-abc1").
		Reply(200).
		AddHeader("X-RateLimit-Remaining-Month", "19").
		AddHeader("X-RateLimit-Limit-Month", "100").
		BodyString(`{"collection":{"info":{"_postman_id":"abc1","name":"test-collection-1","schema":"https://schema.postman.com/json/collection/v2.1.0/collection.json",
		"updatedAt":"2025-03-21T17:39:25.000Z","createdAt":"2025-03-21T17:37:13.000Z","lastUpdatedBy":"1234","uid":"1234-abc1"},
		"item":[]}}`)

	ctx := context.Background()
	s, conn := createTestSource(&sourcespb.Postman{
		Credential: &sourcespb.Postman_Token{
			Token: "super-secret-token",
		},
	})

	err := s.Init(ctx, "test - postman", 0, 1, false, conn, 1)
	if err != nil {
		t.Fatalf("init error: %v", err)
	}
	gock.InterceptClient(s.client.HTTPClient)
	defer gock.RestoreClient(s.client.HTTPClient)

	_, err = s.client.GetCollection(ctx, "1234-abc1")
	if !errors.Is(err, errAbortScanDueToAPIRateLimit) {
		t.Errorf("expected abort scan error due to rate limit, got: %v", err)
	}
}

func TestSource_AbortScanRateLimitBelowThreshold(t *testing.T) {
	defer gock.Off()
	// Mock the API response for getting collection
	gock.New("https://api.getpostman.com").
		Get("/collections/1234-abc1").
		Reply(200).
		AddHeader("X-RateLimit-Remaining-Month", "90").
		AddHeader("X-RateLimit-Limit-Month", "100").
		BodyString(`{"collection":{"info":{"_postman_id":"abc1","name":"test-collection-1","schema":"https://schema.postman.com/json/collection/v2.1.0/collection.json",
		"updatedAt":"2025-03-21T17:39:25.000Z","createdAt":"2025-03-21T17:37:13.000Z","lastUpdatedBy":"1234","uid":"1234-abc1"},
		"item":[]}}`)

	ctx := context.Background()
	s, conn := createTestSource(&sourcespb.Postman{
		Credential: &sourcespb.Postman_Token{
			Token: "super-secret-token",
		},
	})

	err := s.Init(ctx, "test - postman", 0, 1, false, conn, 1)
	if err != nil {
		t.Fatalf("init error: %v", err)
	}
	gock.InterceptClient(s.client.HTTPClient)
	defer gock.RestoreClient(s.client.HTTPClient)

	_, err = s.client.GetCollection(ctx, "1234-abc1")
	if err != nil {
		t.Errorf("expected no error, got: %v", err)
	}
}
