package hhfm_test

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"

	"github.com/apex/log"
	"github.com/google/go-cmp/cmp"
	"github.com/ooni/probe-cli/v3/internal/engine/experiment/hhfm"
	"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
	"github.com/ooni/probe-cli/v3/internal/engine/mockable"
	"github.com/ooni/probe-cli/v3/internal/model"
	"github.com/ooni/probe-cli/v3/internal/netxlite"
	"github.com/ooni/probe-cli/v3/internal/tracex"
)

func TestNewExperimentMeasurer(t *testing.T) {
	measurer := hhfm.NewExperimentMeasurer(hhfm.Config{})
	if measurer.ExperimentName() != "http_header_field_manipulation" {
		t.Fatal("unexpected name")
	}
	if measurer.ExperimentVersion() != "0.2.0" {
		t.Fatal("unexpected version")
	}
}

func TestSuccess(t *testing.T) {
	measurer := hhfm.NewExperimentMeasurer(hhfm.Config{})
	ctx := context.Background()
	sess := &mockable.Session{
		MockableLogger: log.Log,
		MockableTestHelpers: map[string][]model.OOAPIService{
			"http-return-json-headers": {{
				Address: "http://37.218.241.94:80",
				Type:    "legacy",
			}},
		},
	}
	measurement := new(model.Measurement)
	callbacks := model.NewPrinterCallbacks(log.Log)
	err := measurer.Run(ctx, sess, measurement, callbacks)
	if err != nil {
		t.Fatal(err)
	}
	tk := measurement.TestKeys.(*hhfm.TestKeys)
	if tk.Agent != "agent" {
		t.Fatal("invalid Agent")
	}
	if tk.Failure != nil {
		t.Fatal("invalid Failure", *tk.Failure)
	}
	if len(tk.Requests) != 1 {
		t.Fatal("invalid Requests")
	}
	request := tk.Requests[0]
	if request.Failure != nil {
		t.Fatal("invalid Requests[0].Failure")
	}
	if request.Request.Body.Value != "" {
		t.Fatal("invalid Requests[0].Request.Body.Value")
	}
	if request.Request.BodyIsTruncated != false {
		t.Fatal("invalid Requests[0].Request.BodyIsTruncated")
	}
	if len(request.Request.HeadersList) != 6 {
		t.Fatal("invalid Requests[0].Request.HeadersList length")
	}
	if len(request.Request.Headers) != 6 {
		t.Fatal("invalid Requests[0].Request.Headers length")
	}
	if strings.ToUpper(request.Request.Method) != "GET" {
		t.Fatal("invalid Requests[0].Request.Method")
	}
	if request.Request.Tor.ExitIP != nil {
		t.Fatal("invalid Requests[0].Request.Tor.ExitIP")
	}
	if request.Request.Tor.ExitName != nil {
		t.Fatal("invalid Requests[0].Request.Tor.ExitName")
	}
	if request.Request.Tor.IsTor != false {
		t.Fatal("invalid Requests[0].Request.Tor.IsTor")
	}
	ths, ok := sess.GetTestHelpersByName("http-return-json-headers")
	if !ok || len(ths) < 1 || ths[0].Type != "legacy" {
		t.Fatal("cannot get the test helper")
	}
	if request.Request.URL != ths[0].Address {
		t.Fatal("invalid Requests[0].Request.URL")
	}
	if len(request.Response.Body.Value) < 1 {
		t.Fatal("invalid Requests[0].Response.Body.Value length")
	}
	if request.Response.BodyIsTruncated != false {
		t.Fatal("invalid Requests[0].Response.BodyIsTruncated")
	}
	if request.Response.Code != 200 {
		t.Fatal("invalid Requests[0].Code")
	}
	if len(request.Response.HeadersList) != 0 {
		t.Fatal("invalid Requests[0].HeadersList length")
	}
	if len(request.Response.Headers) != 0 {
		t.Fatal("invalid Requests[0].Headers length")
	}
	if request.T != 0 {
		t.Fatal("invalid Requests[0].T")
	}
	if tk.SOCKSProxy != nil {
		t.Fatal("invalid SOCKSProxy")
	}
	if tk.Tampering.HeaderFieldName != false {
		t.Fatal("invalid Tampering.HeaderFieldName")
	}
	if tk.Tampering.HeaderFieldNumber != false {
		t.Fatal("invalid Tampering.HeaderFieldNumber")
	}
	if tk.Tampering.HeaderFieldValue != false {
		t.Fatal("invalid Tampering.HeaderFieldValue")
	}
	if tk.Tampering.HeaderNameCapitalization != false {
		t.Fatal("invalid Tampering.HeaderNameCapitalization")
	}
	if len(tk.Tampering.HeaderNameDiff) != 0 {
		t.Fatal("invalid Tampering.HeaderNameDiff")
	}
	if tk.Tampering.RequestLineCapitalization != false {
		t.Fatal("invalid Tampering.RequestLineCapitalization")
	}
	if tk.Tampering.Total != false {
		t.Fatal("invalid Tampering.Total")
	}
}

func TestCancelledContext(t *testing.T) {
	measurer := hhfm.NewExperimentMeasurer(hhfm.Config{})
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	sess := &mockable.Session{
		MockableLogger: log.Log,
		MockableTestHelpers: map[string][]model.OOAPIService{
			"http-return-json-headers": {{
				Address: "http://37.218.241.94:80",
				Type:    "legacy",
			}},
		},
	}
	measurement := new(model.Measurement)
	callbacks := model.NewPrinterCallbacks(log.Log)
	err := measurer.Run(ctx, sess, measurement, callbacks)
	if err != nil {
		t.Fatal(err)
	}
	tk := measurement.TestKeys.(*hhfm.TestKeys)
	if tk.Agent != "agent" {
		t.Fatal("invalid Agent")
	}
	if *tk.Failure != netxlite.FailureInterrupted {
		t.Fatal("invalid Failure")
	}
	if len(tk.Requests) != 1 {
		t.Fatal("invalid Requests")
	}
	request := tk.Requests[0]
	if *request.Failure != netxlite.FailureInterrupted {
		t.Fatal("invalid Requests[0].Failure")
	}
	if request.Request.Body.Value != "" {
		t.Fatal("invalid Requests[0].Request.Body.Value")
	}
	if request.Request.BodyIsTruncated != false {
		t.Fatal("invalid Requests[0].Request.BodyIsTruncated")
	}
	if len(request.Request.HeadersList) != 6 {
		t.Fatal("invalid Requests[0].Request.HeadersList length")
	}
	if len(request.Request.Headers) != 6 {
		t.Fatal("invalid Requests[0].Request.Headers length")
	}
	if strings.ToUpper(request.Request.Method) != "GET" {
		t.Fatal("invalid Requests[0].Request.Method")
	}
	if request.Request.Tor.ExitIP != nil {
		t.Fatal("invalid Requests[0].Request.Tor.ExitIP")
	}
	if request.Request.Tor.ExitName != nil {
		t.Fatal("invalid Requests[0].Request.Tor.ExitName")
	}
	if request.Request.Tor.IsTor != false {
		t.Fatal("invalid Requests[0].Request.Tor.IsTor")
	}
	ths, ok := sess.GetTestHelpersByName("http-return-json-headers")
	if !ok || len(ths) < 1 || ths[0].Type != "legacy" {
		t.Fatal("cannot get the test helper")
	}
	if request.Request.URL != ths[0].Address {
		t.Fatal("invalid Requests[0].Request.URL")
	}
	if len(request.Response.Body.Value) != 0 {
		t.Fatal("invalid Requests[0].Response.Body.Value length")
	}
	if request.Response.BodyIsTruncated != false {
		t.Fatal("invalid Requests[0].Response.BodyIsTruncated")
	}
	if request.Response.Code != 0 {
		t.Fatal("invalid Requests[0].Code")
	}
	if len(request.Response.HeadersList) != 0 {
		t.Fatal("invalid Requests[0].HeadersList length")
	}
	if len(request.Response.Headers) != 0 {
		t.Fatal("invalid Requests[0].Headers length")
	}
	if request.T != 0 {
		t.Fatal("invalid Requests[0].T")
	}
	if tk.SOCKSProxy != nil {
		t.Fatal("invalid SOCKSProxy")
	}
	if tk.Tampering.HeaderFieldName != false {
		t.Fatal("invalid Tampering.HeaderFieldName")
	}
	if tk.Tampering.HeaderFieldNumber != false {
		t.Fatal("invalid Tampering.HeaderFieldNumber")
	}
	if tk.Tampering.HeaderFieldValue != false {
		t.Fatal("invalid Tampering.HeaderFieldValue")
	}
	if tk.Tampering.HeaderNameCapitalization != false {
		t.Fatal("invalid Tampering.HeaderNameCapitalization")
	}
	if len(tk.Tampering.HeaderNameDiff) != 0 {
		t.Fatal("invalid Tampering.HeaderNameDiff")
	}
	if tk.Tampering.RequestLineCapitalization != false {
		t.Fatal("invalid Tampering.RequestLineCapitalization")
	}
	if tk.Tampering.Total != true {
		t.Fatal("invalid Tampering.Total")
	}
	sk, err := measurer.GetSummaryKeys(measurement)
	if err != nil {
		t.Fatal(err)
	}
	if _, ok := sk.(hhfm.SummaryKeys); !ok {
		t.Fatal("invalid type for summary keys")
	}
}

func TestNoHelpers(t *testing.T) {
	measurer := hhfm.NewExperimentMeasurer(hhfm.Config{})
	ctx := context.Background()
	sess := &mockable.Session{}
	measurement := new(model.Measurement)
	callbacks := model.NewPrinterCallbacks(log.Log)
	err := measurer.Run(ctx, sess, measurement, callbacks)
	if !errors.Is(err, hhfm.ErrNoAvailableTestHelpers) {
		t.Fatal("not the error we expected")
	}
	tk := measurement.TestKeys.(*hhfm.TestKeys)
	if tk.Agent != "agent" {
		t.Fatal("invalid Agent")
	}
	if tk.Failure != nil {
		t.Fatal("invalid Failure")
	}
	if len(tk.Requests) != 0 {
		t.Fatal("invalid Requests")
	}
	if tk.SOCKSProxy != nil {
		t.Fatal("invalid SOCKSProxy")
	}
	if tk.Tampering.HeaderFieldName != false {
		t.Fatal("invalid Tampering.HeaderFieldName")
	}
	if tk.Tampering.HeaderFieldNumber != false {
		t.Fatal("invalid Tampering.HeaderFieldNumber")
	}
	if tk.Tampering.HeaderFieldValue != false {
		t.Fatal("invalid Tampering.HeaderFieldValue")
	}
	if tk.Tampering.HeaderNameCapitalization != false {
		t.Fatal("invalid Tampering.HeaderNameCapitalization")
	}
	if len(tk.Tampering.HeaderNameDiff) != 0 {
		t.Fatal("invalid Tampering.HeaderNameDiff")
	}
	if tk.Tampering.RequestLineCapitalization != false {
		t.Fatal("invalid Tampering.RequestLineCapitalization")
	}
	if tk.Tampering.Total != false {
		t.Fatal("invalid Tampering.Total")
	}
}

func TestNoActualHelpersInList(t *testing.T) {
	measurer := hhfm.NewExperimentMeasurer(hhfm.Config{})
	ctx := context.Background()
	sess := &mockable.Session{
		MockableTestHelpers: map[string][]model.OOAPIService{
			"http-return-json-headers": nil,
		},
	}
	measurement := new(model.Measurement)
	callbacks := model.NewPrinterCallbacks(log.Log)
	err := measurer.Run(ctx, sess, measurement, callbacks)
	if !errors.Is(err, hhfm.ErrNoAvailableTestHelpers) {
		t.Fatal("not the error we expected")
	}
	tk := measurement.TestKeys.(*hhfm.TestKeys)
	if tk.Agent != "agent" {
		t.Fatal("invalid Agent")
	}
	if tk.Failure != nil {
		t.Fatal("invalid Failure")
	}
	if len(tk.Requests) != 0 {
		t.Fatal("invalid Requests")
	}
	if tk.SOCKSProxy != nil {
		t.Fatal("invalid SOCKSProxy")
	}
	if tk.Tampering.HeaderFieldName != false {
		t.Fatal("invalid Tampering.HeaderFieldName")
	}
	if tk.Tampering.HeaderFieldNumber != false {
		t.Fatal("invalid Tampering.HeaderFieldNumber")
	}
	if tk.Tampering.HeaderFieldValue != false {
		t.Fatal("invalid Tampering.HeaderFieldValue")
	}
	if tk.Tampering.HeaderNameCapitalization != false {
		t.Fatal("invalid Tampering.HeaderNameCapitalization")
	}
	if len(tk.Tampering.HeaderNameDiff) != 0 {
		t.Fatal("invalid Tampering.HeaderNameDiff")
	}
	if tk.Tampering.RequestLineCapitalization != false {
		t.Fatal("invalid Tampering.RequestLineCapitalization")
	}
	if tk.Tampering.Total != false {
		t.Fatal("invalid Tampering.Total")
	}
}

func TestWrongTestHelperType(t *testing.T) {
	measurer := hhfm.NewExperimentMeasurer(hhfm.Config{})
	ctx := context.Background()
	sess := &mockable.Session{
		MockableTestHelpers: map[string][]model.OOAPIService{
			"http-return-json-headers": {{
				Address: "http://127.0.0.1",
				Type:    "antani",
			}},
		},
	}
	measurement := new(model.Measurement)
	callbacks := model.NewPrinterCallbacks(log.Log)
	err := measurer.Run(ctx, sess, measurement, callbacks)
	if !errors.Is(err, hhfm.ErrInvalidHelperType) {
		t.Fatal("not the error we expected")
	}
	tk := measurement.TestKeys.(*hhfm.TestKeys)
	if tk.Agent != "agent" {
		t.Fatal("invalid Agent")
	}
	if tk.Failure != nil {
		t.Fatal("invalid Failure")
	}
	if len(tk.Requests) != 0 {
		t.Fatal("invalid Requests")
	}
	if tk.SOCKSProxy != nil {
		t.Fatal("invalid SOCKSProxy")
	}
	if tk.Tampering.HeaderFieldName != false {
		t.Fatal("invalid Tampering.HeaderFieldName")
	}
	if tk.Tampering.HeaderFieldNumber != false {
		t.Fatal("invalid Tampering.HeaderFieldNumber")
	}
	if tk.Tampering.HeaderFieldValue != false {
		t.Fatal("invalid Tampering.HeaderFieldValue")
	}
	if tk.Tampering.HeaderNameCapitalization != false {
		t.Fatal("invalid Tampering.HeaderNameCapitalization")
	}
	if len(tk.Tampering.HeaderNameDiff) != 0 {
		t.Fatal("invalid Tampering.HeaderNameDiff")
	}
	if tk.Tampering.RequestLineCapitalization != false {
		t.Fatal("invalid Tampering.RequestLineCapitalization")
	}
	if tk.Tampering.Total != false {
		t.Fatal("invalid Tampering.Total")
	}
}

func TestNewRequestFailure(t *testing.T) {
	measurer := hhfm.NewExperimentMeasurer(hhfm.Config{})
	ctx := context.Background()
	sess := &mockable.Session{
		MockableTestHelpers: map[string][]model.OOAPIService{
			"http-return-json-headers": {{
				Address: "http://127.0.0.1\t\t\t", // invalid
				Type:    "legacy",
			}},
		},
	}
	measurement := new(model.Measurement)
	callbacks := model.NewPrinterCallbacks(log.Log)
	err := measurer.Run(ctx, sess, measurement, callbacks)
	if err == nil || !strings.HasSuffix(err.Error(), "invalid control character in URL") {
		t.Fatal("not the error we expected")
	}
	tk := measurement.TestKeys.(*hhfm.TestKeys)
	if tk.Agent != "agent" {
		t.Fatal("invalid Agent")
	}
	if tk.Failure != nil {
		t.Fatal("invalid Failure")
	}
	if len(tk.Requests) != 0 {
		t.Fatal("invalid Requests")
	}
	if tk.SOCKSProxy != nil {
		t.Fatal("invalid SOCKSProxy")
	}
	if tk.Tampering.HeaderFieldName != false {
		t.Fatal("invalid Tampering.HeaderFieldName")
	}
	if tk.Tampering.HeaderFieldNumber != false {
		t.Fatal("invalid Tampering.HeaderFieldNumber")
	}
	if tk.Tampering.HeaderFieldValue != false {
		t.Fatal("invalid Tampering.HeaderFieldValue")
	}
	if tk.Tampering.HeaderNameCapitalization != false {
		t.Fatal("invalid Tampering.HeaderNameCapitalization")
	}
	if len(tk.Tampering.HeaderNameDiff) != 0 {
		t.Fatal("invalid Tampering.HeaderNameDiff")
	}
	if tk.Tampering.RequestLineCapitalization != false {
		t.Fatal("invalid Tampering.RequestLineCapitalization")
	}
	if tk.Tampering.Total != false {
		t.Fatal("invalid Tampering.Total")
	}
}

func TestInvalidJSONBody(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello, client") // not valid JSON
	}))
	defer server.Close()
	measurer := hhfm.NewExperimentMeasurer(hhfm.Config{})
	ctx := context.Background()
	sess := &mockable.Session{
		MockableTestHelpers: map[string][]model.OOAPIService{
			"http-return-json-headers": {{
				Address: server.URL,
				Type:    "legacy",
			}},
		},
	}
	measurement := new(model.Measurement)
	callbacks := model.NewPrinterCallbacks(log.Log)
	err := measurer.Run(ctx, sess, measurement, callbacks)
	if err != nil {
		t.Fatal(err)
	}
	tk := measurement.TestKeys.(*hhfm.TestKeys)
	if tk.Agent != "agent" {
		t.Fatal("invalid Agent")
	}
	if *tk.Failure != netxlite.FailureJSONParseError {
		t.Fatal("invalid Failure")
	}
	if len(tk.Requests) != 1 {
		t.Fatal("invalid Requests")
	}
	// we already check the content of Requests in other tests
	if tk.SOCKSProxy != nil {
		t.Fatal("invalid SOCKSProxy")
	}
	if tk.Tampering.HeaderFieldName != false {
		t.Fatal("invalid Tampering.HeaderFieldName")
	}
	if tk.Tampering.HeaderFieldNumber != false {
		t.Fatal("invalid Tampering.HeaderFieldNumber")
	}
	if tk.Tampering.HeaderFieldValue != false {
		t.Fatal("invalid Tampering.HeaderFieldValue")
	}
	if tk.Tampering.HeaderNameCapitalization != false {
		t.Fatal("invalid Tampering.HeaderNameCapitalization")
	}
	if len(tk.Tampering.HeaderNameDiff) != 0 {
		t.Fatal("invalid Tampering.HeaderNameDiff")
	}
	if tk.Tampering.RequestLineCapitalization != false {
		t.Fatal("invalid Tampering.RequestLineCapitalization")
	}
	if tk.Tampering.Total != true {
		t.Fatal("invalid Tampering.Total")
	}
}

func TestTransactStatusCodeFailure(t *testing.T) {
	txp := FakeTransport{Resp: &http.Response{
		Body:       io.NopCloser(strings.NewReader("")),
		StatusCode: 500,
	}}
	resp, body, err := hhfm.Transact(txp, &http.Request{},
		model.NewPrinterCallbacks(log.Log))
	if !errors.Is(err, urlgetter.ErrHTTPRequestFailed) {
		t.Fatal("not the error we expected")
	}
	if resp != nil {
		t.Fatal("resp is not nil")
	}
	if body != nil {
		t.Fatal("body is not nil")
	}
}

func TestTransactCannotReadBody(t *testing.T) {
	expected := errors.New("mocked error")
	txp := FakeTransport{Resp: &http.Response{
		Body:       &FakeBody{Err: expected},
		StatusCode: 200,
	}}
	resp, body, err := hhfm.Transact(txp, &http.Request{},
		model.NewPrinterCallbacks(log.Log))
	if !errors.Is(err, expected) {
		t.Fatal("not the error we expected")
	}
	if resp != nil {
		t.Fatal("resp is not nil")
	}
	if body != nil {
		t.Fatal("body is not nil")
	}
}

func TestTestKeys_FillTampering(t *testing.T) {
	type fields struct {
		Agent      string
		Failure    *string
		Requests   []tracex.RequestEntry
		SOCKSProxy *string
		Tampering  hhfm.Tampering
	}
	type args struct {
		req         *http.Request
		jsonHeaders hhfm.JSONHeaders
		headers     map[string]string
	}
	tests := []struct {
		name   string
		fields fields
		args   args
	}{{
		name: "Request line capitalisation",
		fields: fields{
			Tampering: hhfm.Tampering{
				RequestLineCapitalization: true,
			},
		},
		args: args{
			req: &http.Request{
				Method: "GeT",
			},
			jsonHeaders: hhfm.JSONHeaders{
				RequestLine: "GET / HTTP/1.1",
			},
		},
	}, {
		name: "Header field number",
		fields: fields{
			Tampering: hhfm.Tampering{
				HeaderFieldNumber: true,
			},
		},
		args: args{
			req: &http.Request{
				Method: "GeT",
			},
			jsonHeaders: hhfm.JSONHeaders{
				RequestLine: "GeT / HTTP/1.1",
			},
			headers: map[string]string{
				"UsEr-AgENt": "miniooni/0.1.0-dev",
			},
		},
	}, {
		name: "Header name diff",
		fields: fields{
			Tampering: hhfm.Tampering{
				HeaderNameCapitalization: true,
				HeaderNameDiff:           []string{"UsEr-AgENt", "User-Agent"},
			},
		},
		args: args{
			req: &http.Request{
				Method: "GeT",
			},
			jsonHeaders: hhfm.JSONHeaders{
				RequestLine: "GeT / HTTP/1.1",
				HeadersDict: map[string][]string{
					"User-Agent": {"miniooni/0.1.0-dev"},
				},
			},
			headers: map[string]string{
				"UsEr-AgENt": "miniooni/0.1.0-dev",
			},
		},
	}, {
		name: "Header value diff",
		fields: fields{
			Tampering: hhfm.Tampering{
				HeaderFieldValue: true,
			},
		},
		args: args{
			req: &http.Request{
				Method: "GeT",
			},
			jsonHeaders: hhfm.JSONHeaders{
				RequestLine: "GeT / HTTP/1.1",
				HeadersDict: map[string][]string{
					"UsEr-AgENt": {"MINIOONI/0.1.0-dev"},
				},
			},
			headers: map[string]string{
				"UsEr-AgENt": "miniooni/0.1.0-dev",
			},
		},
	}, {
		name: "Number of headers per key diffs",
		fields: fields{
			Tampering: hhfm.Tampering{
				HeaderFieldValue: true,
			},
		},
		args: args{
			req: &http.Request{
				Method: "GeT",
			},
			jsonHeaders: hhfm.JSONHeaders{
				RequestLine: "GeT / HTTP/1.1",
				HeadersDict: map[string][]string{
					"UsEr-AgENt": {"miniooni/0.1.0-dev", "ooniprobe-engine/0.1.0-dev"},
				},
			},
			headers: map[string]string{
				"UsEr-AgENt": "miniooni/0.1.0-dev",
			},
		},
	}}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tk := &hhfm.TestKeys{
				Agent:      tt.fields.Agent,
				Failure:    tt.fields.Failure,
				Requests:   tt.fields.Requests,
				SOCKSProxy: tt.fields.SOCKSProxy,
			}
			tk.FillTampering(tt.args.req, tt.args.jsonHeaders, tt.args.headers)
			if diff := cmp.Diff(tt.fields.Tampering, tk.Tampering); diff != "" {
				t.Fatal(diff)
			}
		})
	}
}

func TestNewRequestEntryList(t *testing.T) {
	type args struct {
		req     *http.Request
		headers map[string]string
	}
	tests := []struct {
		name    string
		args    args
		wantOut []tracex.RequestEntry
	}{{
		name: "common case",
		args: args{
			req: &http.Request{
				Method: "GeT",
				URL: &url.URL{
					Scheme: "http",
					Host:   "10.0.0.1",
					Path:   "/",
				},
			},
			headers: map[string]string{
				"ContENt-tYPE": "text/plain",
				"User-aGENT":   "foo/1.0",
			},
		},
		wantOut: []tracex.RequestEntry{{
			Request: tracex.HTTPRequest{
				HeadersList: []tracex.HTTPHeader{{
					Key:   "ContENt-tYPE",
					Value: tracex.MaybeBinaryValue{Value: "text/plain"},
				}, {
					Key:   "User-aGENT",
					Value: tracex.MaybeBinaryValue{Value: "foo/1.0"},
				}},
				Headers: map[string]tracex.MaybeBinaryValue{
					"ContENt-tYPE": {Value: "text/plain"},
					"User-aGENT":   {Value: "foo/1.0"},
				},
				Method: "GeT",
				URL:    "http://10.0.0.1/",
			},
		}},
	}, {
		name: "without headers",
		args: args{
			req: &http.Request{
				Method: "GeT",
				URL: &url.URL{
					Scheme: "http",
					Host:   "10.0.0.1",
					Path:   "/",
				},
			},
		},
		wantOut: []tracex.RequestEntry{{
			Request: tracex.HTTPRequest{
				Method:      "GeT",
				Headers:     make(map[string]tracex.MaybeBinaryValue),
				HeadersList: []tracex.HTTPHeader{},
				URL:         "http://10.0.0.1/",
			},
		}},
	}}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gotOut := hhfm.NewRequestEntryList(tt.args.req, tt.args.headers)
			if diff := cmp.Diff(tt.wantOut, gotOut); diff != "" {
				t.Fatal(diff)
			}
		})
	}
}

func TestNewHTTPResponse(t *testing.T) {
	type args struct {
		resp *http.Response
		data []byte
	}
	tests := []struct {
		name    string
		args    args
		wantOut tracex.HTTPResponse
	}{{
		name: "common case",
		args: args{
			resp: &http.Response{
				StatusCode: 200,
				Header: http.Header{
					"Content-Type": []string{"text/plain"},
					"User-Agent":   []string{"foo/1.0"},
				},
			},
			data: []byte("deadbeef"),
		},
		wantOut: tracex.HTTPResponse{
			Body: tracex.MaybeBinaryValue{Value: "deadbeef"},
			Code: 200,
			HeadersList: []tracex.HTTPHeader{{
				Key:   "Content-Type",
				Value: tracex.MaybeBinaryValue{Value: "text/plain"},
			}, {
				Key:   "User-Agent",
				Value: tracex.MaybeBinaryValue{Value: "foo/1.0"},
			}},
			Headers: map[string]tracex.MaybeBinaryValue{
				"Content-Type": {Value: "text/plain"},
				"User-Agent":   {Value: "foo/1.0"},
			},
		},
	}, {
		name: "with no HTTP header and body",
		args: args{
			resp: &http.Response{StatusCode: 200},
		},
		wantOut: tracex.HTTPResponse{
			Body:        tracex.MaybeBinaryValue{Value: ""},
			Code:        200,
			HeadersList: []tracex.HTTPHeader{},
			Headers:     map[string]tracex.MaybeBinaryValue{},
		},
	}}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gotOut := hhfm.NewHTTPResponse(tt.args.resp, tt.args.data)
			if diff := cmp.Diff(tt.wantOut, gotOut); diff != "" {
				t.Fatal(diff)
			}
		})
	}
}

func TestDialerDialContext(t *testing.T) {
	expected := errors.New("mocked error")
	d := hhfm.Dialer{Dialer: FakeDialer{Err: expected}}
	conn, err := d.DialContext(context.Background(), "tcp", "127.0.0.1:80")
	if !errors.Is(err, expected) {
		t.Fatal("not the error we expected")
	}
	if conn != nil {
		t.Fatal("conn is not nil")
	}
}

func TestSummaryKeysInvalidType(t *testing.T) {
	measurement := new(model.Measurement)
	m := &hhfm.Measurer{}
	_, err := m.GetSummaryKeys(measurement)
	if err.Error() != "invalid test keys type" {
		t.Fatal("not the error we expected")
	}
}

func TestSummaryKeysWorksAsIntended(t *testing.T) {
	tests := []struct {
		tampering hhfm.Tampering
		isAnomaly bool
	}{{
		tampering: hhfm.Tampering{},
		isAnomaly: false,
	}, {
		tampering: hhfm.Tampering{HeaderFieldName: true},
		isAnomaly: true,
	}, {
		tampering: hhfm.Tampering{HeaderFieldNumber: true},
		isAnomaly: true,
	}, {
		tampering: hhfm.Tampering{HeaderFieldValue: true},
		isAnomaly: true,
	}, {
		tampering: hhfm.Tampering{HeaderNameCapitalization: true},
		isAnomaly: true,
	}, {
		tampering: hhfm.Tampering{RequestLineCapitalization: true},
		isAnomaly: true,
	}, {
		tampering: hhfm.Tampering{Total: true},
		isAnomaly: true,
	}}
	for idx, tt := range tests {
		t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) {
			m := &hhfm.Measurer{}
			measurement := &model.Measurement{TestKeys: &hhfm.TestKeys{
				Tampering: tt.tampering,
			}}
			got, err := m.GetSummaryKeys(measurement)
			if err != nil {
				t.Fatal(err)
				return
			}
			sk := got.(hhfm.SummaryKeys)
			if sk.IsAnomaly != tt.isAnomaly {
				t.Fatal("unexpected isAnomaly value")
			}
		})
	}
}
