package engine

import (
	"context"
	"errors"
	"net/url"
	"sync"
	"testing"

	"github.com/apex/log"
	"github.com/google/go-cmp/cmp"
	"github.com/ooni/probe-cli/v3/internal/engine/geolocate"
	"github.com/ooni/probe-cli/v3/internal/model"
)

func (s *Session) GetAvailableProbeServices() []model.OOAPIService {
	return s.getAvailableProbeServicesUnlocked()
}

func (s *Session) AppendAvailableProbeService(svc model.OOAPIService) {
	s.availableProbeServices = append(s.availableProbeServices, svc)
}

func (s *Session) QueryProbeServicesCount() int64 {
	return s.queryProbeServicesCount.Load()
}

// mockableProbeServicesClientForCheckIn allows us to mock the
// probeservices.Client used by Session.CheckIn.
type mockableProbeServicesClientForCheckIn struct {
	// Config is the config passed to the call.
	Config *model.OOAPICheckInConfig

	// Results contains the results of the call. This field MUST be
	// non-nil if and only if Error is nil.
	Results *model.OOAPICheckInInfo

	// Error indicates whether the call failed. This field MUST be
	// non-nil if and only if Error is nil.
	Error error

	// mu provides mutual exclusion.
	mu sync.Mutex
}

// CheckIn implements sessionProbeServicesClientForCheckIn.CheckIn.
func (c *mockableProbeServicesClientForCheckIn) CheckIn(
	ctx context.Context, config model.OOAPICheckInConfig) (*model.OOAPICheckInInfo, error) {
	defer c.mu.Unlock()
	c.mu.Lock()
	if c.Config != nil {
		return nil, errors.New("called more than once")
	}
	c.Config = &config
	if c.Results == nil && c.Error == nil {
		return nil, errors.New("misconfigured mockableProbeServicesClientForCheckIn")
	}
	return c.Results, c.Error
}

func TestSessionCheckInSuccessful(t *testing.T) {
	results := &model.OOAPICheckInInfo{
		WebConnectivity: &model.OOAPICheckInInfoWebConnectivity{
			ReportID: "xxx-x-xx",
			URLs: []model.OOAPIURLInfo{{
				CategoryCode: "NEWS",
				CountryCode:  "IT",
				URL:          "https://www.repubblica.it/",
			}, {
				CategoryCode: "NEWS",
				CountryCode:  "IT",
				URL:          "https://www.unita.it/",
			}},
		},
	}
	mockedClnt := &mockableProbeServicesClientForCheckIn{
		Results: results,
	}
	s := &Session{
		location: &geolocate.Results{
			ASN:         137,
			CountryCode: "IT",
		},
		softwareName:    "miniooni",
		softwareVersion: "0.1.0-dev",
		testMaybeLookupLocationContext: func(ctx context.Context) error {
			return nil
		},
		testNewProbeServicesClientForCheckIn: func(
			ctx context.Context) (sessionProbeServicesClientForCheckIn, error) {
			return mockedClnt, nil
		},
	}
	out, err := s.CheckIn(context.Background(), &model.OOAPICheckInConfig{})
	if err != nil {
		t.Fatal(err)
	}
	if diff := cmp.Diff(results, out); diff != "" {
		t.Fatal(diff)
	}
	if mockedClnt.Config.Platform != s.Platform() {
		t.Fatal("invalid Config.Platform")
	}
	if mockedClnt.Config.ProbeASN != "AS137" {
		t.Fatal("invalid Config.ProbeASN")
	}
	if mockedClnt.Config.ProbeCC != "IT" {
		t.Fatal("invalid Config.ProbeCC")
	}
	if mockedClnt.Config.RunType != model.RunTypeTimed {
		t.Fatal("invalid Config.RunType")
	}
	if mockedClnt.Config.SoftwareName != "miniooni" {
		t.Fatal("invalid Config.SoftwareName")
	}
	if mockedClnt.Config.SoftwareVersion != "0.1.0-dev" {
		t.Fatal("invalid Config.SoftwareVersion")
	}
	if mockedClnt.Config.WebConnectivity.CategoryCodes == nil {
		t.Fatal("invalid ...CategoryCodes")
	}
}

func TestSessionCheckInCannotLookupLocation(t *testing.T) {
	errMocked := errors.New("mocked error")
	s := &Session{
		testMaybeLookupLocationContext: func(ctx context.Context) error {
			return errMocked
		},
	}
	out, err := s.CheckIn(context.Background(), &model.OOAPICheckInConfig{})
	if !errors.Is(err, errMocked) {
		t.Fatal("no the error we expected", err)
	}
	if out != nil {
		t.Fatal("expected nil result here")
	}
}

func TestSessionCheckInCannotCreateProbeServicesClient(t *testing.T) {
	errMocked := errors.New("mocked error")
	s := &Session{
		location: &geolocate.Results{
			ASN:         137,
			CountryCode: "IT",
		},
		softwareName:    "miniooni",
		softwareVersion: "0.1.0-dev",
		testMaybeLookupLocationContext: func(ctx context.Context) error {
			return nil
		},
		testNewProbeServicesClientForCheckIn: func(
			ctx context.Context) (sessionProbeServicesClientForCheckIn, error) {
			return nil, errMocked
		},
	}
	out, err := s.CheckIn(context.Background(), &model.OOAPICheckInConfig{})
	if !errors.Is(err, errMocked) {
		t.Fatal("no the error we expected", err)
	}
	if out != nil {
		t.Fatal("expected nil result here")
	}
}

func TestLowercaseMaybeLookupLocationContextWithCancelledContext(t *testing.T) {
	s := &Session{}
	ctx, cancel := context.WithCancel(context.Background())
	cancel() // immediately kill the context
	err := s.maybeLookupLocationContext(ctx)
	if !errors.Is(err, context.Canceled) {
		t.Fatal("not the error we expected", err)
	}
}

func TestNewProbeServicesClientForCheckIn(t *testing.T) {
	s := &Session{}
	ctx, cancel := context.WithCancel(context.Background())
	cancel() // immediately kill the context
	clnt, err := s.newProbeServicesClientForCheckIn(ctx)
	if !errors.Is(err, context.Canceled) {
		t.Fatal("not the error we expected", err)
	}
	if clnt != nil {
		t.Fatal("expected nil client here")
	}
}

func TestSessionNewSubmitterWithCancelledContext(t *testing.T) {
	sess := newSessionForTesting(t)
	ctx, cancel := context.WithCancel(context.Background())
	cancel() // fail immediately
	subm, err := sess.NewSubmitter(ctx)
	if !errors.Is(err, context.Canceled) {
		t.Fatal("not the error we expected", err)
	}
	if subm != nil {
		t.Fatal("expected nil submitter here")
	}
}

func TestSessionMaybeLookupLocationContextLookupLocationContextFailure(t *testing.T) {
	errMocked := errors.New("mocked error")
	sess := newSessionForTestingNoLookups(t)
	sess.testLookupLocationContext = func(ctx context.Context) (*geolocate.Results, error) {
		return nil, errMocked
	}
	err := sess.MaybeLookupLocationContext(context.Background())
	if !errors.Is(err, errMocked) {
		t.Fatal("not the error we expected", err)
	}
}

func TestSessionFetchURLListWithCancelledContext(t *testing.T) {
	sess := &Session{}
	ctx, cancel := context.WithCancel(context.Background())
	cancel() // cause failure
	resp, err := sess.FetchURLList(ctx, model.OOAPIURLListConfig{})
	if !errors.Is(err, context.Canceled) {
		t.Fatal("not the error we expected", err)
	}
	if resp != nil {
		t.Fatal("expected nil response here")
	}
}

func TestSessionFetchTorTargetsWithCancelledContext(t *testing.T) {
	sess := &Session{}
	ctx, cancel := context.WithCancel(context.Background())
	cancel() // cause failure
	resp, err := sess.FetchTorTargets(ctx, "IT")
	if !errors.Is(err, context.Canceled) {
		t.Fatal("not the error we expected", err)
	}
	if resp != nil {
		t.Fatal("expected nil response here")
	}
}

func TestSessionFetchPsiphonConfigWithCancelledContext(t *testing.T) {
	sess := &Session{}
	ctx, cancel := context.WithCancel(context.Background())
	cancel() // cause failure
	resp, err := sess.FetchPsiphonConfig(ctx)
	if !errors.Is(err, context.Canceled) {
		t.Fatal("not the error we expected", err)
	}
	if resp != nil {
		t.Fatal("expected nil response here")
	}
}

func TestNewSessionWithFakeTunnel(t *testing.T) {
	ctx := context.Background()
	sess, err := NewSession(ctx, SessionConfig{
		Logger:          log.Log,
		ProxyURL:        &url.URL{Scheme: "fake"},
		SoftwareName:    "miniooni",
		SoftwareVersion: "0.1.0-dev",
		TunnelDir:       "testdata",
	})
	if err != nil {
		t.Fatal(err)
	}
	if sess == nil {
		t.Fatal("expected non-nil session here")
	}
	if sess.ProxyURL() == nil {
		t.Fatal("expected non-nil proxyURL here")
	}
	if sess.tunnel == nil {
		t.Fatal("expected non-nil tunnel here")
	}
	sess.Close() // ensure we don't crash
}

func TestNewSessionWithFakeTunnelAndCancelledContext(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	cancel() // fail immediately
	sess, err := NewSession(ctx, SessionConfig{
		Logger:          log.Log,
		ProxyURL:        &url.URL{Scheme: "fake"},
		SoftwareName:    "miniooni",
		SoftwareVersion: "0.1.0-dev",
		TunnelDir:       "testdata",
	})
	if !errors.Is(err, context.Canceled) {
		t.Fatal("not the error we expected", err)
	}
	if sess != nil {
		t.Fatal("expected nil session here")
	}
}