package tracex

import (
	"context"
	"errors"
	"io"
	"net/http"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/gorilla/websocket"
	"github.com/ooni/probe-cli/v3/internal/netxlite"
)

func TestDNSQueryType(t *testing.T) {
	t.Run("ipOfType", func(t *testing.T) {
		type expectation struct {
			qtype  dnsQueryType
			ip     string
			output bool
		}
		var expectations = []expectation{{
			qtype:  "A",
			ip:     "8.8.8.8",
			output: true,
		}, {
			qtype:  "A",
			ip:     "2a00:1450:4002:801::2004",
			output: false,
		}, {
			qtype:  "AAAA",
			ip:     "8.8.8.8",
			output: false,
		}, {
			qtype:  "AAAA",
			ip:     "2a00:1450:4002:801::2004",
			output: true,
		}, {
			qtype:  "ANTANI",
			ip:     "2a00:1450:4002:801::2004",
			output: false,
		}, {
			qtype:  "ANTANI",
			ip:     "8.8.8.8",
			output: false,
		}}
		for _, exp := range expectations {
			if exp.qtype.ipOfType(exp.ip) != exp.output {
				t.Fatalf("failure for %+v", exp)
			}
		}
	})
}

func TestNewTCPConnectList(t *testing.T) {
	begin := time.Now()
	type args struct {
		begin  time.Time
		events []Event
	}
	tests := []struct {
		name string
		args args
		want []TCPConnectEntry
	}{{
		name: "empty run",
		args: args{
			begin:  begin,
			events: nil,
		},
		want: nil,
	}, {
		name: "realistic run",
		args: args{
			begin: begin,
			events: []Event{&EventResolveDone{&EventValue{ // skipped because not relevant
				Addresses: []string{"8.8.8.8", "8.8.4.4"},
				Hostname:  "dns.google.com",
				Time:      begin.Add(100 * time.Millisecond),
			}}, &EventConnectOperation{&EventValue{
				Address:  "8.8.8.8:853",
				Duration: 30 * time.Millisecond,
				Proto:    "tcp",
				Time:     begin.Add(130 * time.Millisecond),
			}}, &EventConnectOperation{&EventValue{
				Address:  "8.8.8.8:853",
				Duration: 55 * time.Millisecond,
				Proto:    "udp", // this one should be skipped because it's UDP
				Time:     begin.Add(130 * time.Millisecond),
			}}, &EventConnectOperation{&EventValue{
				Address:  "8.8.4.4:53",
				Duration: 50 * time.Millisecond,
				Err:      netxlite.FailureEOFError,
				Proto:    "tcp",
				Time:     begin.Add(180 * time.Millisecond),
			}}},
		},
		want: []TCPConnectEntry{{
			IP:   "8.8.8.8",
			Port: 853,
			Status: TCPConnectStatus{
				Success: true,
			},
			T: 0.13,
		}, {
			IP:   "8.8.4.4",
			Port: 53,
			Status: TCPConnectStatus{
				Failure: NewFailure(io.EOF),
				Success: false,
			},
			T: 0.18,
		}},
	}}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := NewTCPConnectList(tt.args.begin, tt.args.events)
			if diff := cmp.Diff(tt.want, got); diff != "" {
				t.Fatal(diff)
			}
		})
	}
}

func TestNewRequestList(t *testing.T) {
	begin := time.Now()
	type args struct {
		begin  time.Time
		events []Event
	}
	tests := []struct {
		name string
		args args
		want []RequestEntry
	}{{
		name: "empty run",
		args: args{
			begin:  begin,
			events: nil,
		},
		want: nil,
	}, {
		name: "realistic run",
		args: args{
			begin: begin,
			// Two round trips so we can test the sorting expected by OONI
			events: []Event{&EventHTTPTransactionDone{&EventValue{
				HTTPRequestHeaders: http.Header{
					"User-Agent": []string{"miniooni/0.1.0-dev"},
				},
				HTTPMethod: "POST",
				HTTPURL:    "https://www.example.com/submit",
				HTTPResponseHeaders: http.Header{
					"Server": []string{"miniooni/0.1.0-dev"},
				},
				HTTPStatusCode:              200,
				HTTPResponseBody:            []byte("{}"),
				HTTPResponseBodyIsTruncated: false,
				Time:                        begin.Add(10 * time.Millisecond),
			}}, &EventHTTPTransactionDone{&EventValue{
				HTTPRequestHeaders: http.Header{
					"User-Agent": []string{"miniooni/0.1.0-dev"},
				},
				HTTPMethod: "GET",
				HTTPURL:    "https://www.example.com/result",
				Err:        netxlite.FailureEOFError,
				Time:       begin.Add(20 * time.Millisecond),
			}}},
		},
		want: []RequestEntry{{
			Failure: NewFailure(io.EOF),
			Request: HTTPRequest{
				HeadersList: []HTTPHeader{{
					Key: "User-Agent",
					Value: MaybeBinaryValue{
						Value: "miniooni/0.1.0-dev",
					},
				}},
				Headers: map[string]MaybeBinaryValue{
					"User-Agent": {Value: "miniooni/0.1.0-dev"},
				},
				Method: "GET",
				URL:    "https://www.example.com/result",
			},
			Response: HTTPResponse{
				HeadersList: []HTTPHeader{},
				Headers:     make(map[string]MaybeBinaryValue),
			},
			T: 0.02,
		}, {
			Request: HTTPRequest{
				Body: MaybeBinaryValue{
					Value: "",
				},
				HeadersList: []HTTPHeader{{
					Key: "User-Agent",
					Value: MaybeBinaryValue{
						Value: "miniooni/0.1.0-dev",
					},
				}},
				Headers: map[string]MaybeBinaryValue{
					"User-Agent": {Value: "miniooni/0.1.0-dev"},
				},
				Method: "POST",
				URL:    "https://www.example.com/submit",
			},
			Response: HTTPResponse{
				Body: MaybeBinaryValue{
					Value: "{}",
				},
				Code: 200,
				HeadersList: []HTTPHeader{{
					Key: "Server",
					Value: MaybeBinaryValue{
						Value: "miniooni/0.1.0-dev",
					},
				}},
				Headers: map[string]MaybeBinaryValue{
					"Server": {Value: "miniooni/0.1.0-dev"},
				},
				Locations: nil,
			},
			T: 0.01,
		}},
	}, {
		// for an example of why we need to sort headers, see
		// https://github.com/ooni/probe-engine/pull/751/checks?check_run_id=853562310
		name: "run with redirect and headers to sort",
		args: args{
			begin: begin,
			events: []Event{&EventHTTPTransactionDone{&EventValue{
				HTTPRequestHeaders: http.Header{
					"User-Agent": []string{"miniooni/0.1.0-dev"},
				},
				HTTPMethod: "GET",
				HTTPURL:    "https://www.example.com/",
				HTTPResponseHeaders: http.Header{
					"Server":   []string{"miniooni/0.1.0-dev"},
					"Location": []string{"https://x.example.com", "https://y.example.com"},
				},
				HTTPStatusCode: 302,
				Time:           begin.Add(10 * time.Millisecond),
			}}},
		},
		want: []RequestEntry{{
			Request: HTTPRequest{
				HeadersList: []HTTPHeader{{
					Key: "User-Agent",
					Value: MaybeBinaryValue{
						Value: "miniooni/0.1.0-dev",
					},
				}},
				Headers: map[string]MaybeBinaryValue{
					"User-Agent": {Value: "miniooni/0.1.0-dev"},
				},
				Method: "GET",
				URL:    "https://www.example.com/",
			},
			Response: HTTPResponse{
				Code: 302,
				HeadersList: []HTTPHeader{{
					Key: "Location",
					Value: MaybeBinaryValue{
						Value: "https://x.example.com",
					},
				}, {
					Key: "Location",
					Value: MaybeBinaryValue{
						Value: "https://y.example.com",
					},
				}, {
					Key: "Server",
					Value: MaybeBinaryValue{
						Value: "miniooni/0.1.0-dev",
					},
				}},
				Headers: map[string]MaybeBinaryValue{
					"Server":   {Value: "miniooni/0.1.0-dev"},
					"Location": {Value: "https://x.example.com"},
				},
				Locations: []string{
					"https://x.example.com", "https://y.example.com",
				},
			},
			T: 0.01,
		}},
	}}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := NewRequestList(tt.args.begin, tt.args.events)
			if diff := cmp.Diff(tt.want, got); diff != "" {
				t.Fatal(diff)
			}
		})
	}
}

func TestNewDNSQueriesList(t *testing.T) {
	begin := time.Now()
	type args struct {
		begin  time.Time
		events []Event
	}
	tests := []struct {
		name string
		args args
		want []DNSQueryEntry
	}{{
		name: "empty run",
		args: args{
			begin:  begin,
			events: nil,
		},
		want: nil,
	}, {
		name: "realistic run",
		args: args{
			begin: begin,
			events: []Event{&EventResolveDone{&EventValue{
				Address:   "1.1.1.1:853",
				Addresses: []string{"8.8.8.8", "8.8.4.4"},
				Hostname:  "dns.google.com",
				Proto:     "dot",
				Time:      begin.Add(100 * time.Millisecond),
			}}, &EventConnectOperation{&EventValue{ // skipped because not relevant
				Address:  "8.8.8.8:853",
				Duration: 30 * time.Millisecond,
				Proto:    "tcp",
				Time:     begin.Add(130 * time.Millisecond),
			}}, &EventConnectOperation{&EventValue{ // skipped because not relevant
				Address:  "8.8.4.4:53",
				Duration: 50 * time.Millisecond,
				Err:      netxlite.FailureEOFError,
				Proto:    "tcp",
				Time:     begin.Add(180 * time.Millisecond),
			}}},
		},
		want: []DNSQueryEntry{{
			Answers: []DNSAnswerEntry{{
				ASN:        15169,
				ASOrgName:  "Google LLC",
				AnswerType: "A",
				IPv4:       "8.8.8.8",
			}, {
				ASN:        15169,
				ASOrgName:  "Google LLC",
				AnswerType: "A",
				IPv4:       "8.8.4.4",
			}},
			Engine:          "dot",
			Hostname:        "dns.google.com",
			QueryType:       "A",
			ResolverAddress: "1.1.1.1:853",
			T:               0.1,
		}},
	}, {
		name: "run with IPv6 results",
		args: args{
			begin: begin,
			events: []Event{&EventResolveDone{&EventValue{
				Addresses: []string{"2001:4860:4860::8888"},
				Hostname:  "dns.google.com",
				Time:      begin.Add(200 * time.Millisecond),
			}}},
		},
		want: []DNSQueryEntry{{
			Answers: []DNSAnswerEntry{{
				ASN:        15169,
				ASOrgName:  "Google LLC",
				AnswerType: "AAAA",
				IPv6:       "2001:4860:4860::8888",
			}},
			Hostname:  "dns.google.com",
			QueryType: "AAAA",
			T:         0.2,
		}},
	}, {
		name: "run with errors",
		args: args{
			begin: begin,
			events: []Event{&EventResolveDone{&EventValue{
				Err:      netxlite.FailureDNSNXDOMAINError,
				Hostname: "dns.google.com",
				Time:     begin.Add(200 * time.Millisecond),
			}}},
		},
		want: []DNSQueryEntry{{
			Answers: nil,
			Failure: NewFailure(
				&netxlite.ErrWrapper{Failure: netxlite.FailureDNSNXDOMAINError}),
			Hostname:  "dns.google.com",
			QueryType: "A",
			T:         0.2,
		}, {
			Answers: nil,
			Failure: NewFailure(
				&netxlite.ErrWrapper{Failure: netxlite.FailureDNSNXDOMAINError}),
			Hostname:  "dns.google.com",
			QueryType: "AAAA",
			T:         0.2,
		}},
	}}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := NewDNSQueriesList(tt.args.begin, tt.args.events)
			if diff := cmp.Diff(tt.want, got); diff != "" {
				t.Fatal(diff)
			}
		})
	}
}

func TestNewNetworkEventsList(t *testing.T) {
	begin := time.Now()
	type args struct {
		begin  time.Time
		events []Event
	}
	tests := []struct {
		name string
		args args
		want []NetworkEvent
	}{{
		name: "empty run",
		args: args{
			begin:  begin,
			events: nil,
		},
		want: nil,
	}, {
		name: "realistic run",
		args: args{
			begin: begin,
			events: []Event{&EventConnectOperation{&EventValue{
				Address: "8.8.8.8:853",
				Err:     netxlite.FailureEOFError,
				Proto:   "tcp",
				Time:    begin.Add(7 * time.Millisecond),
			}}, &EventReadOperation{&EventValue{
				Err:      netxlite.FailureInterrupted,
				NumBytes: 7117,
				Time:     begin.Add(11 * time.Millisecond),
			}}, &EventReadFromOperation{&EventValue{
				Address:  "8.8.8.8:853",
				Err:      netxlite.FailureInterrupted,
				NumBytes: 7117,
				Time:     begin.Add(11 * time.Millisecond),
			}}, &EventWriteOperation{&EventValue{
				Err:      NewFailureStr(websocket.ErrBadHandshake),
				NumBytes: 4114,
				Time:     begin.Add(14 * time.Millisecond),
			}}, &EventWriteToOperation{&EventValue{
				Address:  "8.8.8.8:853",
				Err:      NewFailureStr(websocket.ErrBadHandshake),
				NumBytes: 4114,
				Time:     begin.Add(14 * time.Millisecond),
			}}, &EventResolveStart{&EventValue{
				// We expect this event to be logged event though it's not a typical I/O
				// event (it seems these extra events are useful for debugging)
				Time: begin.Add(15 * time.Millisecond),
			}}},
		},
		want: []NetworkEvent{{
			Address:   "8.8.8.8:853",
			Failure:   NewFailure(io.EOF),
			Operation: netxlite.ConnectOperation,
			Proto:     "tcp",
			T:         0.007,
		}, {
			Failure:   NewFailure(context.Canceled),
			NumBytes:  7117,
			Operation: netxlite.ReadOperation,
			T:         0.011,
		}, {
			Address:   "8.8.8.8:853",
			Failure:   NewFailure(context.Canceled),
			NumBytes:  7117,
			Operation: netxlite.ReadFromOperation,
			T:         0.011,
		}, {
			Failure:   NewFailure(websocket.ErrBadHandshake),
			NumBytes:  4114,
			Operation: netxlite.WriteOperation,
			T:         0.014,
		}, {
			Address:   "8.8.8.8:853",
			Failure:   NewFailure(websocket.ErrBadHandshake),
			NumBytes:  4114,
			Operation: netxlite.WriteToOperation,
			T:         0.014,
		}, {
			Operation: "resolve_start",
			T:         0.015,
		}},
	}}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := NewNetworkEventsList(tt.args.begin, tt.args.events)
			if diff := cmp.Diff(tt.want, got); diff != "" {
				t.Fatal(diff)
			}
		})
	}
}

func TestNewTLSHandshakesList(t *testing.T) {
	begin := time.Now()
	type args struct {
		begin  time.Time
		events []Event
	}
	tests := []struct {
		name string
		args args
		want []TLSHandshake
	}{{
		name: "empty run",
		args: args{
			begin:  begin,
			events: nil,
		},
		want: nil,
	}, {
		name: "realistic run with TLS",
		args: args{
			begin: begin,
			events: []Event{&EventTLSHandshakeDone{&EventValue{
				Address:            "131.252.210.176:443",
				Err:                netxlite.FailureEOFError,
				NoTLSVerify:        false,
				Proto:              "tcp",
				TLSCipherSuite:     "SUITE",
				TLSNegotiatedProto: "h2",
				TLSPeerCerts: [][]byte{
					[]byte("deadbeef"),
					[]byte("abad1dea"),
				},
				TLSServerName: "x.org",
				TLSVersion:    "TLSv1.3",
				Time:          begin.Add(55 * time.Millisecond),
			}}},
		},
		want: []TLSHandshake{{
			Address:            "131.252.210.176:443",
			CipherSuite:        "SUITE",
			Failure:            NewFailure(io.EOF),
			NegotiatedProtocol: "h2",
			NoTLSVerify:        false,
			PeerCertificates: []MaybeBinaryValue{{
				Value: "deadbeef",
			}, {
				Value: "abad1dea",
			}},
			ServerName: "x.org",
			T:          0.055,
			TLSVersion: "TLSv1.3",
		}},
	}, {
		name: "realistic run with QUIC",
		args: args{
			begin: begin,
			events: []Event{&EventQUICHandshakeDone{&EventValue{
				Address:            "131.252.210.176:443",
				Err:                netxlite.FailureEOFError,
				NoTLSVerify:        false,
				Proto:              "udp",
				TLSCipherSuite:     "SUITE",
				TLSNegotiatedProto: "h3",
				TLSPeerCerts: [][]byte{
					[]byte("deadbeef"),
					[]byte("abad1dea"),
				},
				TLSServerName: "x.org",
				TLSVersion:    "TLSv1.3",
				Time:          begin.Add(55 * time.Millisecond),
			}}},
		},
		want: []TLSHandshake{{
			Address:            "131.252.210.176:443",
			CipherSuite:        "SUITE",
			Failure:            NewFailure(io.EOF),
			NegotiatedProtocol: "h3",
			NoTLSVerify:        false,
			PeerCertificates: []MaybeBinaryValue{{
				Value: "deadbeef",
			}, {
				Value: "abad1dea",
			}},
			ServerName: "x.org",
			T:          0.055,
			TLSVersion: "TLSv1.3",
		}},
	}, {
		name: "realistic run with no suitable events",
		args: args{
			begin: begin,
			events: []Event{&EventResolveStart{&EventValue{
				Time: begin.Add(55 * time.Millisecond),
			}}},
		},
		want: nil,
	}}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := NewTLSHandshakesList(tt.args.begin, tt.args.events)
			if diff := cmp.Diff(tt.want, got); diff != "" {
				t.Fatal(diff)
			}
		})
	}
}

func TestNewFailure(t *testing.T) {
	type args struct {
		err error
	}
	tests := []struct {
		name string
		args args
		want *string
	}{{
		name: "when error is nil",
		args: args{
			err: nil,
		},
		want: nil,
	}, {
		name: "when error is wrapped and failure meaningful",
		args: args{
			err: &netxlite.ErrWrapper{
				Failure: netxlite.FailureConnectionRefused,
			},
		},
		want: func() *string {
			s := netxlite.FailureConnectionRefused
			return &s
		}(),
	}, {
		name: "when error is wrapped and failure is not meaningful",
		args: args{
			err: &netxlite.ErrWrapper{},
		},
		want: func() *string {
			s := "unknown_failure: errWrapper.Failure is empty"
			return &s
		}(),
	}, {
		name: "when error is not wrapped but wrappable",
		args: args{err: io.EOF},
		want: func() *string {
			s := "eof_error"
			return &s
		}(),
	}, {
		name: "when the error is not wrapped and not wrappable",
		args: args{
			err: errors.New("use of closed socket 127.0.0.1:8080->10.0.0.1:22"),
		},
		want: func() *string {
			s := "unknown_failure: use of closed socket [scrubbed]->[scrubbed]"
			return &s
		}(),
	}}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := NewFailure(tt.args.err)
			if tt.want == nil && got == nil {
				return
			}
			if tt.want == nil && got != nil {
				t.Errorf("NewFailure:  want %+v, got %s", tt.want, *got)
				return
			}
			if tt.want != nil && got == nil {
				t.Errorf("NewFailure:  want %s, got %+v", *tt.want, got)
				return
			}
			if *tt.want != *got {
				t.Errorf("NewFailure:  want %s, got %s", *tt.want, *got)
				return
			}
		})
	}
}

func TestNewFailedOperation(t *testing.T) {
	type args struct {
		err error
	}
	tests := []struct {
		name string
		args args
		want *string
	}{{
		name: "With no error",
		args: args{
			err: nil, // explicit
		},
		want: nil, // explicit
	}, {
		name: "With wrapped error and non-empty operation",
		args: args{
			err: &netxlite.ErrWrapper{
				Failure:   netxlite.FailureConnectionRefused,
				Operation: netxlite.ConnectOperation,
			},
		},
		want: (func() *string {
			s := netxlite.ConnectOperation
			return &s
		})(),
	}, {
		name: "With wrapped error and empty operation",
		args: args{
			err: &netxlite.ErrWrapper{
				Failure: netxlite.FailureConnectionRefused,
			},
		},
		want: (func() *string {
			s := netxlite.UnknownOperation
			return &s
		})(),
	}, {
		name: "With non wrapped error",
		args: args{
			err: io.EOF,
		},
		want: (func() *string {
			s := netxlite.UnknownOperation
			return &s
		})(),
	}}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := NewFailedOperation(tt.args.err)
			if got == nil && tt.want == nil {
				return
			}
			if got == nil && tt.want != nil {
				t.Errorf("NewFailedOperation() = %v, want %v", got, tt.want)
				return
			}
			if got != nil && tt.want == nil {
				t.Errorf("NewFailedOperation() = %v, want %v", got, tt.want)
				return
			}
			if got != nil && tt.want != nil && *got != *tt.want {
				t.Errorf("NewFailedOperation() = %v, want %v", got, tt.want)
				return
			}
		})
	}
}
