package measurexlite

import (
	"net"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/ooni/probe-cli/v3/internal/model"
	"github.com/ooni/probe-cli/v3/internal/model/mocks"
	"github.com/ooni/probe-cli/v3/internal/netxlite"
	"github.com/ooni/probe-cli/v3/internal/testingx"
)

func TestMaybeClose(t *testing.T) {
	t.Run("with nil conn", func(t *testing.T) {
		var conn net.Conn = nil
		MaybeClose(conn)
	})

	t.Run("with nonnil conn", func(t *testing.T) {
		var called bool
		conn := &mocks.Conn{
			MockClose: func() error {
				called = true
				return nil
			},
		}
		if err := MaybeClose(conn); err != nil {
			t.Fatal(err)
		}
		if !called {
			t.Fatal("not called")
		}
	})
}

func TestWrapNetConn(t *testing.T) {
	t.Run("WrapNetConn wraps the conn", func(t *testing.T) {
		underlying := &mocks.Conn{}
		zeroTime := time.Now()
		trace := NewTrace(0, zeroTime)
		conn := trace.MaybeWrapNetConn(underlying)
		ct := conn.(*connTrace)
		if ct.Conn != underlying {
			t.Fatal("invalid underlying")
		}
		if ct.tx != trace {
			t.Fatal("invalid trace")
		}
	})

	t.Run("Read saves a trace", func(t *testing.T) {
		underlying := &mocks.Conn{
			MockRead: func(b []byte) (int, error) {
				return len(b), nil
			},
			MockRemoteAddr: func() net.Addr {
				return &mocks.Addr{
					MockNetwork: func() string {
						return "tcp"
					},
					MockString: func() string {
						return "1.1.1.1:443"
					},
				}
			},
		}
		zeroTime := time.Now()
		td := testingx.NewTimeDeterministic(zeroTime)
		trace := NewTrace(0, zeroTime)
		trace.TimeNowFn = td.Now // deterministic time counting
		conn := trace.MaybeWrapNetConn(underlying)
		const bufsiz = 128
		buffer := make([]byte, bufsiz)
		count, err := conn.Read(buffer)
		if count != bufsiz {
			t.Fatal("invalid count")
		}
		if err != nil {
			t.Fatal("invalid err")
		}
		events := trace.NetworkEvents()
		if len(events) != 1 {
			t.Fatal("did not save network events")
		}
		expect := &model.ArchivalNetworkEvent{
			Address:   "1.1.1.1:443",
			Failure:   nil,
			NumBytes:  bufsiz,
			Operation: netxlite.ReadOperation,
			Proto:     "tcp",
			T:         1.0,
			Tags:      []string{},
		}
		got := events[0]
		if diff := cmp.Diff(expect, got); diff != "" {
			t.Fatal(diff)
		}
	})

	t.Run("Read discards the event when the buffer is full", func(t *testing.T) {
		underlying := &mocks.Conn{
			MockRead: func(b []byte) (int, error) {
				return len(b), nil
			},
			MockRemoteAddr: func() net.Addr {
				return &mocks.Addr{
					MockNetwork: func() string {
						return "tcp"
					},
					MockString: func() string {
						return "1.1.1.1:443"
					},
				}
			},
		}
		zeroTime := time.Now()
		trace := NewTrace(0, zeroTime)
		trace.networkEvent = make(chan *model.ArchivalNetworkEvent) // no buffer
		conn := trace.MaybeWrapNetConn(underlying)
		const bufsiz = 128
		buffer := make([]byte, bufsiz)
		count, err := conn.Read(buffer)
		if count != bufsiz {
			t.Fatal("invalid count")
		}
		if err != nil {
			t.Fatal("invalid err")
		}
		events := trace.NetworkEvents()
		if len(events) != 0 {
			t.Fatal("expected no network events")
		}
	})

	t.Run("Write saves a trace", func(t *testing.T) {
		underlying := &mocks.Conn{
			MockWrite: func(b []byte) (int, error) {
				return len(b), nil
			},
			MockRemoteAddr: func() net.Addr {
				return &mocks.Addr{
					MockNetwork: func() string {
						return "tcp"
					},
					MockString: func() string {
						return "1.1.1.1:443"
					},
				}
			},
		}
		zeroTime := time.Now()
		td := testingx.NewTimeDeterministic(zeroTime)
		trace := NewTrace(0, zeroTime)
		trace.TimeNowFn = td.Now // deterministic time tracking
		conn := trace.MaybeWrapNetConn(underlying)
		const bufsiz = 128
		buffer := make([]byte, bufsiz)
		count, err := conn.Write(buffer)
		if count != bufsiz {
			t.Fatal("invalid count")
		}
		if err != nil {
			t.Fatal("invalid err")
		}
		events := trace.NetworkEvents()
		if len(events) != 1 {
			t.Fatal("did not save network events")
		}
		expect := &model.ArchivalNetworkEvent{
			Address:   "1.1.1.1:443",
			Failure:   nil,
			NumBytes:  bufsiz,
			Operation: netxlite.WriteOperation,
			Proto:     "tcp",
			T:         1.0,
			Tags:      []string{},
		}
		got := events[0]
		if diff := cmp.Diff(expect, got); diff != "" {
			t.Fatal(diff)
		}
	})

	t.Run("Write discards the event when the buffer is full", func(t *testing.T) {
		underlying := &mocks.Conn{
			MockWrite: func(b []byte) (int, error) {
				return len(b), nil
			},
			MockRemoteAddr: func() net.Addr {
				return &mocks.Addr{
					MockNetwork: func() string {
						return "tcp"
					},
					MockString: func() string {
						return "1.1.1.1:443"
					},
				}
			},
		}
		zeroTime := time.Now()
		trace := NewTrace(0, zeroTime)
		trace.networkEvent = make(chan *model.ArchivalNetworkEvent) // no buffer
		conn := trace.MaybeWrapNetConn(underlying)
		const bufsiz = 128
		buffer := make([]byte, bufsiz)
		count, err := conn.Write(buffer)
		if count != bufsiz {
			t.Fatal("invalid count")
		}
		if err != nil {
			t.Fatal("invalid err")
		}
		events := trace.NetworkEvents()
		if len(events) != 0 {
			t.Fatal("expected no network events")
		}
	})
}

func TestWrapUDPLikeConn(t *testing.T) {
	t.Run("WrapUDPLikeConn wraps the conn", func(t *testing.T) {
		underlying := &mocks.UDPLikeConn{}
		zeroTime := time.Now()
		trace := NewTrace(0, zeroTime)
		conn := trace.MaybeWrapUDPLikeConn(underlying)
		ct := conn.(*udpLikeConnTrace)
		if ct.UDPLikeConn != underlying {
			t.Fatal("invalid underlying")
		}
		if ct.tx != trace {
			t.Fatal("invalid trace")
		}
	})

	t.Run("ReadFrom saves a trace", func(t *testing.T) {
		underlying := &mocks.UDPLikeConn{
			MockReadFrom: func(b []byte) (int, net.Addr, error) {
				return len(b), &mocks.Addr{
					MockString: func() string {
						return "1.1.1.1:443"
					},
				}, nil
			},
		}
		zeroTime := time.Now()
		td := testingx.NewTimeDeterministic(zeroTime)
		trace := NewTrace(0, zeroTime)
		trace.TimeNowFn = td.Now // deterministic time counting
		conn := trace.MaybeWrapUDPLikeConn(underlying)
		const bufsiz = 128
		buffer := make([]byte, bufsiz)
		count, addr, err := conn.ReadFrom(buffer)
		if count != bufsiz {
			t.Fatal("invalid count")
		}
		if addr.String() != "1.1.1.1:443" {
			t.Fatal("invalid address")
		}
		if err != nil {
			t.Fatal("invalid err")
		}
		events := trace.NetworkEvents()
		if len(events) != 1 {
			t.Fatal("did not save network events")
		}
		expect := &model.ArchivalNetworkEvent{
			Address:   "1.1.1.1:443",
			Failure:   nil,
			NumBytes:  bufsiz,
			Operation: "read_from",
			Proto:     "udp",
			T:         1.0,
			Tags:      []string{},
		}
		got := events[0]
		if diff := cmp.Diff(expect, got); diff != "" {
			t.Fatal(diff)
		}
	})

	t.Run("ReadFrom discards the event when the buffer is full", func(t *testing.T) {
		underlying := &mocks.UDPLikeConn{
			MockReadFrom: func(b []byte) (int, net.Addr, error) {
				return len(b), &mocks.Addr{
					MockString: func() string {
						return "1.1.1.1:443"
					},
				}, nil
			},
		}
		zeroTime := time.Now()
		trace := NewTrace(0, zeroTime)
		trace.networkEvent = make(chan *model.ArchivalNetworkEvent) // no buffer
		conn := trace.MaybeWrapUDPLikeConn(underlying)
		const bufsiz = 128
		buffer := make([]byte, bufsiz)
		count, addr, err := conn.ReadFrom(buffer)
		if count != bufsiz {
			t.Fatal("invalid count")
		}
		if addr.String() != "1.1.1.1:443" {
			t.Fatal("invalid address")
		}
		if err != nil {
			t.Fatal("invalid err")
		}
		events := trace.NetworkEvents()
		if len(events) != 0 {
			t.Fatal("expected no network events")
		}
	})

	t.Run("WriteTo saves a trace", func(t *testing.T) {
		underlying := &mocks.UDPLikeConn{
			MockWriteTo: func(b []byte, addr net.Addr) (int, error) {
				return len(b), nil
			},
		}
		zeroTime := time.Now()
		td := testingx.NewTimeDeterministic(zeroTime)
		trace := NewTrace(0, zeroTime)
		trace.TimeNowFn = td.Now // deterministic time tracking
		conn := trace.MaybeWrapUDPLikeConn(underlying)
		const bufsiz = 128
		buffer := make([]byte, bufsiz)
		addr := &mocks.Addr{
			MockString: func() string {
				return "1.1.1.1:443"
			},
		}
		count, err := conn.WriteTo(buffer, addr)
		if count != bufsiz {
			t.Fatal("invalid count")
		}
		if err != nil {
			t.Fatal("invalid err")
		}
		events := trace.NetworkEvents()
		if len(events) != 1 {
			t.Fatal("did not save network events")
		}
		expect := &model.ArchivalNetworkEvent{
			Address:   "1.1.1.1:443",
			Failure:   nil,
			NumBytes:  bufsiz,
			Operation: "write_to",
			Proto:     "udp",
			T:         1.0,
			Tags:      []string{},
		}
		got := events[0]
		if diff := cmp.Diff(expect, got); diff != "" {
			t.Fatal(diff)
		}
	})

	t.Run("Write discards the event when the buffer is full", func(t *testing.T) {
		underlying := &mocks.UDPLikeConn{
			MockWriteTo: func(b []byte, addr net.Addr) (int, error) {
				return len(b), nil
			},
		}
		zeroTime := time.Now()
		trace := NewTrace(0, zeroTime)
		trace.networkEvent = make(chan *model.ArchivalNetworkEvent) // no buffer
		conn := trace.MaybeWrapUDPLikeConn(underlying)
		const bufsiz = 128
		buffer := make([]byte, bufsiz)
		addr := &mocks.Addr{
			MockString: func() string {
				return "1.1.1.1:443"
			},
		}
		count, err := conn.WriteTo(buffer, addr)
		if count != bufsiz {
			t.Fatal("invalid count")
		}
		if err != nil {
			t.Fatal("invalid err")
		}
		events := trace.NetworkEvents()
		if len(events) != 0 {
			t.Fatal("expected no network events")
		}
	})
}

func TestFirstNetworkEvent(t *testing.T) {
	t.Run("returns nil when buffer is empty", func(t *testing.T) {
		zeroTime := time.Now()
		trace := NewTrace(0, zeroTime)
		got := trace.FirstNetworkEventOrNil()
		if got != nil {
			t.Fatal("expected nil event")
		}
	})

	t.Run("return first non-nil network event", func(t *testing.T) {
		filler := func(tx *Trace, events []*model.ArchivalNetworkEvent) {
			for _, ev := range events {
				tx.networkEvent <- ev
			}
		}
		zeroTime := time.Now()
		trace := NewTrace(0, zeroTime)
		expect := []*model.ArchivalNetworkEvent{{
			Address:   "1.1.1.1:443",
			Failure:   nil,
			NumBytes:  0,
			Operation: "read_from",
			Proto:     "udp",
			T:         1.0,
			Tags:      []string{},
		}, {
			Address:   "1.1.1.1:443",
			Failure:   nil,
			NumBytes:  0,
			Operation: "write_to",
			Proto:     "udp",
			T:         1.0,
			Tags:      []string{},
		}}
		filler(trace, expect)
		got := trace.FirstNetworkEventOrNil()
		if diff := cmp.Diff(got, expect[0]); diff != "" {
			t.Fatal(diff)
		}
	})
}

func TestNewAnnotationArchivalNetworkEvent(t *testing.T) {
	var (
		index     int64 = 3
		duration        = 250 * time.Millisecond
		operation       = "tls_handshake_start"
	)
	expect := &model.ArchivalNetworkEvent{
		Address:       "",
		Failure:       nil,
		NumBytes:      0,
		Operation:     operation,
		Proto:         "",
		T0:            duration.Seconds(),
		T:             duration.Seconds(),
		TransactionID: index,
		Tags:          []string{},
	}
	got := NewAnnotationArchivalNetworkEvent(
		index, duration, operation,
	)
	if diff := cmp.Diff(expect, got); diff != "" {
		t.Fatal(diff)
	}
}
