package discovery

import (
	"context"
	"net/http"
	"reflect"
	"testing"
	"time"

	"codeberg.org/eduVPN/eduvpn-common/internal/test"
	discotypes "codeberg.org/eduVPN/eduvpn-common/types/discovery"
)

// TestServers tests whether or not we can obtain discovery servers
// It setups up a file server using the 'test_files' directory
func TestServers(t *testing.T) {
	handler := http.FileServer(http.Dir("test_files"))
	s := test.NewServer(handler, nil)
	DiscoURL = s.URL
	c, err := s.Client()
	if err != nil {
		t.Fatalf("Failed to get HTTP test client: %v", err)
	}
	d := &Discovery{httpClient: c}
	// get servers
	_, fresh, err := d.Servers(context.Background(), false)
	if !fresh {
		t.Fatalf("Did not obtain the server list fresh")
	}
	if err != nil {
		t.Fatalf("Failed getting servers: %v", err)
	}

	// force expired
	d.ServerList.Timestamp = time.Now().Add(-1 * time.Hour)

	// override the last server with a keyword list
	// and make sure a new fetch doesn't copy over the keyword list
	// to the last entry
	d.ServerList.List[len(d.ServerList.List)-1] = Server{
		Server: discotypes.Server{
			BaseURL: "https://example.org/",
			DisplayName: map[string]string{
				"en": "example",
			},
			Type: "institute_access",
		},
		KeywordList: map[string]string{
			"en": "test bla",
		},
		SupportContact: []string{"mailto:test@example.org"},
	}
	// conditional requests: this should not be fetched fresh
	_, fresh, err = d.Servers(context.Background(), false)
	if fresh {
		t.Fatalf("Obtained the server list fresh with conditional requests")
	}
	if err != nil {
		t.Fatalf("Failed getting servers after inserting a mock entry: %v", err)
	}
	// mock conditional requests
	d.ServerList.UpdateHeader = time.Time{}
	s1, fresh, err := d.Servers(context.Background(), false)
	if !fresh {
		t.Fatalf("Did not obtain the server list fresh with mocked conditional request and mocked entry")
	}
	if err != nil {
		t.Fatalf("Failed getting servers after inserting a mock entry and mocking conditional request: %v", err)
	}
	if kws := s1.List[len(s1.List)-1].KeywordList; kws != nil {
		t.Fatalf("KeywordList is not nil when getting a fresh server list after inserting a mock entry: %v", kws)
	}

	// Shutdown the server
	s.Close()
	// Test if we get the same cached copy
	s2, fresh, err := d.Servers(context.Background(), false)
	// We should not get an error as the timestamp is not expired
	if fresh {
		t.Fatalf("The server list was obtained fresh")
	}
	if err != nil {
		t.Fatalf("Got a servers error after shutting down server: %v", err)
	}
	if s1 != s2 {
		t.Fatalf("Servers copies not equal after shutting down file server")
	}

	// Force expired, 1 hour in the past
	// we should return the previous with an error
	d.ServerList.Timestamp = time.Now().Add(-1 * time.Hour)

	s3, fresh, err := d.Servers(context.Background(), false)
	if fresh {
		t.Fatalf("Server list was gotten fresh")
	}
	// Now we expect an error with the cached copy
	if err == nil {
		t.Fatalf("Got a servers nil error after shutting down file server and expired")
	}
	if s1 != s3 {
		t.Fatalf("Servers copies not equal after shutting down file server and expired")
	}
}

// TestOrganizations tests whether or not we can obtain discovery organizations
// It setups up a file server using the 'test_files' directory
func TestOrganizations(t *testing.T) {
	handler := http.FileServer(http.Dir("test_files"))
	s := test.NewServer(handler, nil)
	DiscoURL = s.URL
	c, err := s.Client()
	if err != nil {
		t.Fatalf("Failed to get HTTP test client: %v", err)
	}
	d := &Discovery{httpClient: c}
	// get servers
	_, fresh, err := d.Organizations(context.Background(), false)
	if !fresh {
		t.Fatalf("The organization list was not obtained fresh")
	}
	if err != nil {
		t.Fatalf("Failed getting organizations: %v", err)
	}
	if !fresh {
		t.Fatalf("Did not get a fresh organization list")
	}

	// force expired
	d.OrganizationList.Timestamp = time.Now().Add(-4 * time.Hour)

	// override the last organization with a keyword list
	// and make sure a new fetch doesn't copy over the keyword list
	// to the last entry
	d.OrganizationList.List[len(d.OrganizationList.List)-1] = Organization{
		Organization: discotypes.Organization{
			DisplayName: map[string]string{
				"en": "example",
			},
			OrgID: "example_orgid",
		},
		SecureInternetHome: "example.org",
		KeywordList: map[string]string{
			"en": "test bla",
		},
	}

	_, fresh, err = d.Organizations(context.Background(), false)
	if fresh {
		t.Fatalf("Obtained the organization list fresh with conditional requests")
	}
	if err != nil {
		t.Fatalf("Failed getting organizations after inserting a mock entry: %v", err)
	}
	// mock conditional requests
	d.OrganizationList.UpdateHeader = time.Time{}
	s1, fresh, err := d.Organizations(context.Background(), false)
	if !fresh {
		t.Fatalf("Did not obtain the organization list fresh after inserting a mock entry, faking expiry and mocking conditional request")
	}
	if err != nil {
		t.Fatalf("Failed getting organizations after inserting a mock entry and faking conditional request: %v", err)
	}
	if kws := s1.List[len(s1.List)-1].KeywordList; kws != nil {
		t.Fatalf("KeywordList is not nil when getting a fresh organization list after inserting a mock entry: %v", kws)
	}

	// Shutdown the server
	s.Close()
	// Test if we get the same cached copy
	// We should not get an error as the timestamp is not zero
	s2, fresh, err := d.Organizations(context.Background(), false)
	if fresh {
		t.Fatalf("The organization list is freshly obtained")
	}
	if err != nil {
		t.Fatalf("Got an organizations error after shutting down file server: %v", err)
	}
	if s1 != s2 {
		t.Fatalf("Organizations copies not equal after shutting down file server")
	}
}

// TestSecureLocationList tests the function for getting a list of secure internet servers
func TestSecureLocationList(t *testing.T) {
	d := Discovery{
		ServerList: Servers{
			Version: 1,
			List: []Server{
				// institute access server, this should not be found
				{Server: discotypes.Server{Type: "institute_access"}},
				// secure internet servers, these should be found
				{Server: discotypes.Server{Type: "secure_internet", CountryCode: "b"}},
				{Server: discotypes.Server{Type: "secure_internet", CountryCode: "c"}},
				// Unexpected type, this should not be found
				{Server: discotypes.Server{Type: "test", CountryCode: "d"}},
			},
		},
	}

	cc := d.SecureLocationList()
	want := []string{"b", "c"}

	if !reflect.DeepEqual(cc, want) {
		t.Fatalf("Secure location list is not equal. Got: %v, Want: %v", cc, want)
	}
}

// TestServerByURL tests the function for getting a server by the Base URL and type
func TestServerByURL(t *testing.T) {
	d := Discovery{
		ServerList: Servers{
			Version: 1,
			List: []Server{
				// institute access server
				{Server: discotypes.Server{BaseURL: "a", Type: "institute_access"}},
				// secure internet servers
				{Server: discotypes.Server{BaseURL: "b", Type: "secure_internet"}},
				// Unexpected type, this should not be found
				{Server: discotypes.Server{BaseURL: "d", Type: "test"}},
			},
		},
	}
	// Institute Access: Can be found
	_, err := d.ServerByURL("a", "institute_access")
	if err != nil {
		t.Fatalf("Got error: %v, when getting a server by url with parameters 'a' and 'institute_access'", err)
	}

	// Institute Access: Cannot be found
	_, err = d.ServerByURL("b", "institute_access")
	if err == nil {
		t.Fatal("Got no error, when getting a non-existing server by url with parameters 'b' and 'institute_access'")
	}

	// Secure Internet: Can be found
	_, err = d.ServerByURL("b", "secure_internet")
	if err != nil {
		t.Fatalf("Got error: %v, when getting a server by url with parameters 'b' and 'secure_internet'", err)
	}

	// Secure Internet: Cannot be found because of invalid type
	_, err = d.ServerByURL("d", "secure_internet")
	if err == nil {
		t.Fatal("Got no error, when getting a non-existing server by url with parameters 'd' and 'secure_internet'")
	}
}

// TestServerByCountryCode tests the function for getting a server by the country code
func TestServerByCountryCode(t *testing.T) {
	s1 := Server{Server: discotypes.Server{Type: "secure_internet", CountryCode: "a"}}
	d := Discovery{
		ServerList: Servers{
			Version: 1,
			List: []Server{
				// secure internet server
				s1,
				// Unexpected types, these should not be found
				{Server: discotypes.Server{Type: "institute_access", CountryCode: "b"}},
				{Server: discotypes.Server{Type: "test", CountryCode: "c"}},
			},
		},
	}
	// Institute Access: Can be found
	s, err := d.ServerByCountryCode("a")
	if err != nil {
		t.Fatalf("Got error: %v, when getting a server by country code 'a'", err)
	}
	if s.CountryCode != s1.CountryCode || s.Type != s1.Type {
		t.Fatalf("Server with country code 'a' not equal, Got: %v, Want: %v", s, s1)
	}

	// Others: Cannot be found
	_, err = d.ServerByCountryCode("b")
	if err == nil {
		t.Fatal("Got no error when getting a server by country code 'b'")
	}
	_, err = d.ServerByCountryCode("c")
	if err == nil {
		t.Fatal("Got no error when getting a server by country code 'c'")
	}
}

// TestOrgByID tests the function for getting an organization by ID
func TestOrgByID(t *testing.T) {
	o1 := discotypes.Organization{OrgID: "a"}
	d := Discovery{
		OrganizationList: Organizations{
			Version: 1,
			List: []Organization{
				{Organization: o1},
			},
		},
	}
	o, err := d.orgByID("a")
	if err != nil {
		t.Fatal("Got an error when getting an organization with ID: 'a'")
	}
	if o.OrgID != o1.OrgID {
		t.Fatalf("Organizations not equal, Got: %v, Want: %v", o, o1)
	}
	_, err = d.orgByID("b")
	if err == nil {
		t.Fatal("Got no error when searching for non-existing organization 'b'")
	}
}

// TestSecureHomeArgs tests the function for getting an organization and matching secure internet server by organization ID
func TestSecureHomeArgs(t *testing.T) {
	o1 := Organization{Organization: discotypes.Organization{OrgID: "id"}, SecureInternetHome: "a"}
	s1 := discotypes.Server{BaseURL: "a", Type: "secure_internet"}
	d := Discovery{
		OrganizationList: Organizations{
			Version: 1,
			List: []Organization{
				{Organization: discotypes.Organization{OrgID: "id2"}, SecureInternetHome: "c"},
				o1,
			},
		},
		ServerList: Servers{
			Version: 1,
			List: []Server{
				{Server: s1},
				{Server: discotypes.Server{BaseURL: "b"}},
			},
		},
	}

	// Args found
	o, s, err := d.SecureHomeArgs("id")
	if err != nil {
		t.Fatalf("Got error: %v, when getting secure home arguments with ID: 'id'", err)
	}
	if o.OrgID != o1.OrgID || o.SecureInternetHome != o1.SecureInternetHome {
		t.Fatalf("Organizations not equal for secure home arguments, Got: %v, Want: %v", o, o1)
	}
	if s.BaseURL != s1.BaseURL {
		t.Fatalf("Servers not equal for secure home arguments, Got: %v, Want: %v", s, s1)
	}
	// Args not found because no matching secure internet server
	_, _, err = d.SecureHomeArgs("id2")
	if err == nil {
		t.Fatal("Got no error, when getting non-matching secure home arguments with ID: 'id2'")
	}

	// Args not found because no organization
	_, _, err = d.SecureHomeArgs("id3")
	if err == nil {
		t.Fatal("Got no error, when getting non-existing secure home arguments with ID: 'id3'")
	}
}
