package sql

import (
	"errors"
	"fmt"
	"testing"
	"time"

	"github.com/TwiN/gatus/v5/alerting/alert"
	"github.com/TwiN/gatus/v5/config/endpoint"
	"github.com/TwiN/gatus/v5/storage/store/common"
	"github.com/TwiN/gatus/v5/storage/store/common/paging"
)

var (
	firstCondition  = endpoint.Condition("[STATUS] == 200")
	secondCondition = endpoint.Condition("[RESPONSE_TIME] < 500")
	thirdCondition  = endpoint.Condition("[CERTIFICATE_EXPIRATION] < 72h")

	now = time.Now()

	testEndpoint = endpoint.Endpoint{
		Name:                    "name",
		Group:                   "group",
		URL:                     "https://example.org/what/ever",
		Method:                  "GET",
		Body:                    "body",
		Interval:                30 * time.Second,
		Conditions:              []endpoint.Condition{firstCondition, secondCondition, thirdCondition},
		Alerts:                  nil,
		NumberOfFailuresInARow:  0,
		NumberOfSuccessesInARow: 0,
	}
	testSuccessfulResult = endpoint.Result{
		Hostname:              "example.org",
		IP:                    "127.0.0.1",
		HTTPStatus:            200,
		Errors:                nil,
		Connected:             true,
		Success:               true,
		Timestamp:             now,
		Duration:              150 * time.Millisecond,
		CertificateExpiration: 10 * time.Hour,
		ConditionResults: []*endpoint.ConditionResult{
			{
				Condition: "[STATUS] == 200",
				Success:   true,
			},
			{
				Condition: "[RESPONSE_TIME] < 500",
				Success:   true,
			},
			{
				Condition: "[CERTIFICATE_EXPIRATION] < 72h",
				Success:   true,
			},
		},
	}
	testUnsuccessfulResult = endpoint.Result{
		Hostname:              "example.org",
		IP:                    "127.0.0.1",
		HTTPStatus:            200,
		Errors:                []string{"error-1", "error-2"},
		Connected:             true,
		Success:               false,
		Timestamp:             now,
		Duration:              750 * time.Millisecond,
		CertificateExpiration: 10 * time.Hour,
		ConditionResults: []*endpoint.ConditionResult{
			{
				Condition: "[STATUS] == 200",
				Success:   true,
			},
			{
				Condition: "[RESPONSE_TIME] < 500",
				Success:   false,
			},
			{
				Condition: "[CERTIFICATE_EXPIRATION] < 72h",
				Success:   false,
			},
		},
	}
)

func TestNewStore(t *testing.T) {
	if _, err := NewStore("", t.TempDir()+"/TestNewStore.db", false); !errors.Is(err, ErrDatabaseDriverNotSpecified) {
		t.Error("expected error due to blank driver parameter")
	}
	if _, err := NewStore("sqlite", "", false); !errors.Is(err, ErrPathNotSpecified) {
		t.Error("expected error due to blank path parameter")
	}
	if store, err := NewStore("sqlite", t.TempDir()+"/TestNewStore.db", true); err != nil {
		t.Error("shouldn't have returned any error, got", err.Error())
	} else {
		_ = store.db.Close()
	}
}

func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) {
	store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InsertCleansUpOldUptimeEntriesProperly.db", false)
	defer store.Close()
	now := time.Now().Truncate(time.Hour)
	now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())

	store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-5 * time.Hour), Success: true})

	tx, _ := store.db.Begin()
	oldest, _ := store.getAgeOfOldestEndpointUptimeEntry(tx, 1)
	_ = tx.Commit()
	if oldest.Truncate(time.Hour) != 5*time.Hour {
		t.Errorf("oldest endpoint uptime entry should've been ~5 hours old, was %s", oldest)
	}

	// The oldest cache entry should remain at ~5 hours old, because this entry is more recent
	store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-3 * time.Hour), Success: true})

	tx, _ = store.db.Begin()
	oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1)
	_ = tx.Commit()
	if oldest.Truncate(time.Hour) != 5*time.Hour {
		t.Errorf("oldest endpoint uptime entry should've been ~5 hours old, was %s", oldest)
	}

	// The oldest cache entry should now become at ~8 hours old, because this entry is older
	store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-8 * time.Hour), Success: true})

	tx, _ = store.db.Begin()
	oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1)
	_ = tx.Commit()
	if oldest.Truncate(time.Hour) != 8*time.Hour {
		t.Errorf("oldest endpoint uptime entry should've been ~8 hours old, was %s", oldest)
	}

	// Since this is one hour before reaching the clean up threshold, the oldest entry should now be this one
	store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-(uptimeAgeCleanUpThreshold - time.Hour)), Success: true})

	tx, _ = store.db.Begin()
	oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1)
	_ = tx.Commit()
	if oldest.Truncate(time.Hour) != uptimeAgeCleanUpThreshold-time.Hour {
		t.Errorf("oldest endpoint uptime entry should've been ~%s hours old, was %s", uptimeAgeCleanUpThreshold-time.Hour, oldest)
	}

	// Since this entry is after the uptimeAgeCleanUpThreshold, both this entry as well as the previous
	// one should be deleted since they both surpass uptimeRetention
	store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-(uptimeAgeCleanUpThreshold + time.Hour)), Success: true})

	tx, _ = store.db.Begin()
	oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1)
	_ = tx.Commit()
	if oldest.Truncate(time.Hour) != 8*time.Hour {
		t.Errorf("oldest endpoint uptime entry should've been ~8 hours old, was %s", oldest)
	}
}

func TestStore_HourlyUptimeEntriesAreMergedIntoDailyUptimeEntriesProperly(t *testing.T) {
	store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_HourlyUptimeEntriesAreMergedIntoDailyUptimeEntriesProperly.db", false)
	defer store.Close()
	now := time.Now().Truncate(time.Hour)
	now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())

	scenarios := []struct {
		numberOfHours            int
		expectedMaxUptimeEntries int64
	}{
		{numberOfHours: 1, expectedMaxUptimeEntries: 1},
		{numberOfHours: 10, expectedMaxUptimeEntries: 10},
		{numberOfHours: 50, expectedMaxUptimeEntries: 50},
		{numberOfHours: 75, expectedMaxUptimeEntries: 75},
		{numberOfHours: 99, expectedMaxUptimeEntries: 99},
		{numberOfHours: 150, expectedMaxUptimeEntries: 100},
		{numberOfHours: 300, expectedMaxUptimeEntries: 100},
		{numberOfHours: 768, expectedMaxUptimeEntries: 100}, // 32 days (in hours), which means anything beyond that won't be persisted anyway
		{numberOfHours: 1000, expectedMaxUptimeEntries: 100},
	}
	// Note that is not technically an accurate real world representation, because uptime entries are always added in
	// the present, while this test is inserting results from the past to simulate long term uptime entries.
	// Since we want to test the behavior and not the test itself, this is a "best effort" approach.
	for _, scenario := range scenarios {
		t.Run(fmt.Sprintf("num-hours-%d-expected-max-entries-%d", scenario.numberOfHours, scenario.expectedMaxUptimeEntries), func(t *testing.T) {
			for i := scenario.numberOfHours; i > 0; i-- {
				//fmt.Printf("i: %d (%s)\n", i, now.Add(-time.Duration(i)*time.Hour))
				// Create an uptime entry
				err := store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-time.Duration(i) * time.Hour), Success: true})
				if err != nil {
					t.Log(err)
				}
				//// DEBUGGING: check number of uptime entries for endpoint
				//tx, _ := store.db.Begin()
				//numberOfUptimeEntriesForEndpoint, err := store.getNumberOfUptimeEntriesByEndpointID(tx, 1)
				//if err != nil {
				//	t.Log(err)
				//}
				//_ = tx.Commit()
				//t.Logf("i=%d; numberOfHours=%d; There are currently %d uptime entries for endpointID=%d", i, scenario.numberOfHours, numberOfUptimeEntriesForEndpoint, 1)
			}
			// check number of uptime entries for endpoint
			tx, _ := store.db.Begin()
			numberOfUptimeEntriesForEndpoint, err := store.getNumberOfUptimeEntriesByEndpointID(tx, 1)
			if err != nil {
				t.Log(err)
			}
			_ = tx.Commit()
			//t.Logf("numberOfHours=%d; There are currently %d uptime entries for endpointID=%d", scenario.numberOfHours, numberOfUptimeEntriesForEndpoint, 1)
			if scenario.expectedMaxUptimeEntries < numberOfUptimeEntriesForEndpoint {
				t.Errorf("expected %d (uptime entries) to be smaller than %d", numberOfUptimeEntriesForEndpoint, scenario.expectedMaxUptimeEntries)
			}
			store.Clear()
		})
	}
}

func TestStore_getEndpointUptime(t *testing.T) {
	store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InsertCleansUpEventsAndResultsProperly.db", false)
	defer store.Clear()
	defer store.Close()
	// Add 768 hourly entries (32 days)
	// Daily entries should be merged from hourly entries automatically
	for i := 768; i > 0; i-- {
		err := store.Insert(&testEndpoint, &endpoint.Result{Timestamp: time.Now().Add(-time.Duration(i) * time.Hour), Duration: time.Second, Success: true})
		if err != nil {
			t.Log(err)
		}
	}
	// Check the number of uptime entries
	tx, _ := store.db.Begin()
	numberOfUptimeEntriesForEndpoint, err := store.getNumberOfUptimeEntriesByEndpointID(tx, 1)
	if err != nil {
		t.Log(err)
	}
	if numberOfUptimeEntriesForEndpoint < 20 || numberOfUptimeEntriesForEndpoint > 200 {
		t.Errorf("expected number of uptime entries to be between 20 and 200, got %d", numberOfUptimeEntriesForEndpoint)
	}
	// Retrieve uptime for the past 30d
	uptime, avgResponseTime, err := store.getEndpointUptime(tx, 1, time.Now().Add(-(30 * 24 * time.Hour)), time.Now())
	if err != nil {
		t.Log(err)
	}
	_ = tx.Commit()
	if avgResponseTime != time.Second {
		t.Errorf("expected average response time to be %s, got %s", time.Second, avgResponseTime)
	}
	if uptime != 1 {
		t.Errorf("expected uptime to be 1, got %f", uptime)
	}
	// Add a new unsuccessful result, which should impact the uptime
	err = store.Insert(&testEndpoint, &endpoint.Result{Timestamp: time.Now(), Duration: time.Second, Success: false})
	if err != nil {
		t.Log(err)
	}
	// Retrieve uptime for the past 30d
	tx, _ = store.db.Begin()
	uptime, _, err = store.getEndpointUptime(tx, 1, time.Now().Add(-(30 * 24 * time.Hour)), time.Now())
	if err != nil {
		t.Log(err)
	}
	_ = tx.Commit()
	if uptime == 1 {
		t.Errorf("expected uptime to be less than 1, got %f", uptime)
	}
	// Retrieve uptime for the past 30d, but excluding the last 24h
	// This is not a real use case as there is no way for users to exclude the last 24h, but this is a great way
	// to ensure that hourly merging works as intended
	tx, _ = store.db.Begin()
	uptimeExcludingLast24h, _, err := store.getEndpointUptime(tx, 1, time.Now().Add(-(30 * 24 * time.Hour)), time.Now().Add(-24*time.Hour))
	if err != nil {
		t.Log(err)
	}
	_ = tx.Commit()
	if uptimeExcludingLast24h == uptime {
		t.Error("expected uptimeExcludingLast24h to to be different from uptime, got")
	}
}

func TestStore_InsertCleansUpEventsAndResultsProperly(t *testing.T) {
	store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InsertCleansUpEventsAndResultsProperly.db", false)
	defer store.Clear()
	defer store.Close()
	for i := 0; i < resultsCleanUpThreshold+eventsCleanUpThreshold; i++ {
		store.Insert(&testEndpoint, &testSuccessfulResult)
		store.Insert(&testEndpoint, &testUnsuccessfulResult)
		ss, _ := store.GetEndpointStatusByKey(testEndpoint.Key(), paging.NewEndpointStatusParams().WithResults(1, common.MaximumNumberOfResults*5).WithEvents(1, common.MaximumNumberOfEvents*5))
		if len(ss.Results) > resultsCleanUpThreshold+1 {
			t.Errorf("number of results shouldn't have exceeded %d, reached %d", resultsCleanUpThreshold, len(ss.Results))
		}
		if len(ss.Events) > eventsCleanUpThreshold+1 {
			t.Errorf("number of events shouldn't have exceeded %d, reached %d", eventsCleanUpThreshold, len(ss.Events))
		}
	}
}

func TestStore_InsertWithCaching(t *testing.T) {
	store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InsertWithCaching.db", true)
	defer store.Close()
	// Add 2 results
	store.Insert(&testEndpoint, &testSuccessfulResult)
	store.Insert(&testEndpoint, &testSuccessfulResult)
	// Verify that they exist
	endpointStatuses, _ := store.GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(1, 20))
	if numberOfEndpointStatuses := len(endpointStatuses); numberOfEndpointStatuses != 1 {
		t.Fatalf("expected 1 EndpointStatus, got %d", numberOfEndpointStatuses)
	}
	if len(endpointStatuses[0].Results) != 2 {
		t.Fatalf("expected 2 results, got %d", len(endpointStatuses[0].Results))
	}
	// Add 2 more results
	store.Insert(&testEndpoint, &testUnsuccessfulResult)
	store.Insert(&testEndpoint, &testUnsuccessfulResult)
	// Verify that they exist
	endpointStatuses, _ = store.GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(1, 20))
	if numberOfEndpointStatuses := len(endpointStatuses); numberOfEndpointStatuses != 1 {
		t.Fatalf("expected 1 EndpointStatus, got %d", numberOfEndpointStatuses)
	}
	if len(endpointStatuses[0].Results) != 4 {
		t.Fatalf("expected 4 results, got %d", len(endpointStatuses[0].Results))
	}
	// Clear the store, which should also clear the cache
	store.Clear()
	// Verify that they no longer exist
	endpointStatuses, _ = store.GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(1, 20))
	if numberOfEndpointStatuses := len(endpointStatuses); numberOfEndpointStatuses != 0 {
		t.Fatalf("expected 0 EndpointStatus, got %d", numberOfEndpointStatuses)
	}
}

func TestStore_Persistence(t *testing.T) {
	path := t.TempDir() + "/TestStore_Persistence.db"
	store, _ := NewStore("sqlite", path, false)
	store.Insert(&testEndpoint, &testSuccessfulResult)
	store.Insert(&testEndpoint, &testUnsuccessfulResult)
	if uptime, _ := store.GetUptimeByKey(testEndpoint.Key(), time.Now().Add(-time.Hour), time.Now()); uptime != 0.5 {
		t.Errorf("the uptime over the past 1h should've been 0.5, got %f", uptime)
	}
	if uptime, _ := store.GetUptimeByKey(testEndpoint.Key(), time.Now().Add(-time.Hour*24), time.Now()); uptime != 0.5 {
		t.Errorf("the uptime over the past 24h should've been 0.5, got %f", uptime)
	}
	if uptime, _ := store.GetUptimeByKey(testEndpoint.Key(), time.Now().Add(-time.Hour*24*7), time.Now()); uptime != 0.5 {
		t.Errorf("the uptime over the past 7d should've been 0.5, got %f", uptime)
	}
	if uptime, _ := store.GetUptimeByKey(testEndpoint.Key(), time.Now().Add(-time.Hour*24*30), time.Now()); uptime != 0.5 {
		t.Errorf("the uptime over the past 30d should've been 0.5, got %f", uptime)
	}
	ssFromOldStore, _ := store.GetEndpointStatus(testEndpoint.Group, testEndpoint.Name, paging.NewEndpointStatusParams().WithResults(1, common.MaximumNumberOfResults).WithEvents(1, common.MaximumNumberOfEvents))
	if ssFromOldStore == nil || ssFromOldStore.Group != "group" || ssFromOldStore.Name != "name" || len(ssFromOldStore.Events) != 3 || len(ssFromOldStore.Results) != 2 {
		store.Close()
		t.Fatal("sanity check failed")
	}
	store.Close()
	store, _ = NewStore("sqlite", path, false)
	defer store.Close()
	ssFromNewStore, _ := store.GetEndpointStatus(testEndpoint.Group, testEndpoint.Name, paging.NewEndpointStatusParams().WithResults(1, common.MaximumNumberOfResults).WithEvents(1, common.MaximumNumberOfEvents))
	if ssFromNewStore == nil || ssFromNewStore.Group != "group" || ssFromNewStore.Name != "name" || len(ssFromNewStore.Events) != 3 || len(ssFromNewStore.Results) != 2 {
		t.Fatal("failed sanity check")
	}
	if ssFromNewStore == ssFromOldStore {
		t.Fatal("ss from the old and new store should have a different memory address")
	}
	for i := range ssFromNewStore.Events {
		if ssFromNewStore.Events[i].Timestamp != ssFromOldStore.Events[i].Timestamp {
			t.Error("new and old should've been the same")
		}
		if ssFromNewStore.Events[i].Type != ssFromOldStore.Events[i].Type {
			t.Error("new and old should've been the same")
		}
	}
	for i := range ssFromOldStore.Results {
		if ssFromNewStore.Results[i].Timestamp != ssFromOldStore.Results[i].Timestamp {
			t.Error("new and old should've been the same")
		}
		if ssFromNewStore.Results[i].Success != ssFromOldStore.Results[i].Success {
			t.Error("new and old should've been the same")
		}
		if ssFromNewStore.Results[i].Connected != ssFromOldStore.Results[i].Connected {
			t.Error("new and old should've been the same")
		}
		if ssFromNewStore.Results[i].IP != ssFromOldStore.Results[i].IP {
			t.Error("new and old should've been the same")
		}
		if ssFromNewStore.Results[i].Hostname != ssFromOldStore.Results[i].Hostname {
			t.Error("new and old should've been the same")
		}
		if ssFromNewStore.Results[i].HTTPStatus != ssFromOldStore.Results[i].HTTPStatus {
			t.Error("new and old should've been the same")
		}
		if ssFromNewStore.Results[i].DNSRCode != ssFromOldStore.Results[i].DNSRCode {
			t.Error("new and old should've been the same")
		}
		if len(ssFromNewStore.Results[i].Errors) != len(ssFromOldStore.Results[i].Errors) {
			t.Error("new and old should've been the same")
		} else {
			for j := range ssFromOldStore.Results[i].Errors {
				if ssFromNewStore.Results[i].Errors[j] != ssFromOldStore.Results[i].Errors[j] {
					t.Error("new and old should've been the same")
				}
			}
		}
		if len(ssFromNewStore.Results[i].ConditionResults) != len(ssFromOldStore.Results[i].ConditionResults) {
			t.Error("new and old should've been the same")
		} else {
			for j := range ssFromOldStore.Results[i].ConditionResults {
				if ssFromNewStore.Results[i].ConditionResults[j].Condition != ssFromOldStore.Results[i].ConditionResults[j].Condition {
					t.Error("new and old should've been the same")
				}
				if ssFromNewStore.Results[i].ConditionResults[j].Success != ssFromOldStore.Results[i].ConditionResults[j].Success {
					t.Error("new and old should've been the same")
				}
			}
		}
	}
}

func TestStore_Save(t *testing.T) {
	store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_Save.db", false)
	defer store.Close()
	if store.Save() != nil {
		t.Error("Save shouldn't do anything for this store")
	}
}

// Note that are much more extensive tests in /storage/store/store_test.go.
// This test is simply an extra sanity check
func TestStore_SanityCheck(t *testing.T) {
	store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_SanityCheck.db", false)
	defer store.Close()
	store.Insert(&testEndpoint, &testSuccessfulResult)
	endpointStatuses, _ := store.GetAllEndpointStatuses(paging.NewEndpointStatusParams())
	if numberOfEndpointStatuses := len(endpointStatuses); numberOfEndpointStatuses != 1 {
		t.Fatalf("expected 1 EndpointStatus, got %d", numberOfEndpointStatuses)
	}
	store.Insert(&testEndpoint, &testUnsuccessfulResult)
	// Both results inserted are for the same endpoint, therefore, the count shouldn't have increased
	endpointStatuses, _ = store.GetAllEndpointStatuses(paging.NewEndpointStatusParams())
	if numberOfEndpointStatuses := len(endpointStatuses); numberOfEndpointStatuses != 1 {
		t.Fatalf("expected 1 EndpointStatus, got %d", numberOfEndpointStatuses)
	}
	if hourlyAverageResponseTime, err := store.GetHourlyAverageResponseTimeByKey(testEndpoint.Key(), time.Now().Add(-24*time.Hour), time.Now()); err != nil {
		t.Errorf("expected no error, got %v", err)
	} else if len(hourlyAverageResponseTime) != 1 {
		t.Errorf("expected 1 hour to have had a result in the past 24 hours, got %d", len(hourlyAverageResponseTime))
	}
	if uptime, _ := store.GetUptimeByKey(testEndpoint.Key(), time.Now().Add(-24*time.Hour), time.Now()); uptime != 0.5 {
		t.Errorf("expected uptime of last 24h to be 0.5, got %f", uptime)
	}
	if averageResponseTime, _ := store.GetAverageResponseTimeByKey(testEndpoint.Key(), time.Now().Add(-24*time.Hour), time.Now()); averageResponseTime != 450 {
		t.Errorf("expected average response time of last 24h to be 450, got %d", averageResponseTime)
	}
	ss, _ := store.GetEndpointStatus(testEndpoint.Group, testEndpoint.Name, paging.NewEndpointStatusParams().WithResults(1, 20).WithEvents(1, 20))
	if ss == nil {
		t.Fatalf("Store should've had key '%s', but didn't", testEndpoint.Key())
	}
	if len(ss.Events) != 3 {
		t.Errorf("Endpoint '%s' should've had 3 events, got %d", ss.Name, len(ss.Events))
	}
	if len(ss.Results) != 2 {
		t.Errorf("Endpoint '%s' should've had 2 results, got %d", ss.Name, len(ss.Results))
	}
	if deleted := store.DeleteAllEndpointStatusesNotInKeys([]string{"invalid-key-which-means-everything-should-get-deleted"}); deleted != 1 {
		t.Errorf("%d entries should've been deleted, got %d", 1, deleted)
	}
	if deleted := store.DeleteAllEndpointStatusesNotInKeys([]string{}); deleted != 0 {
		t.Errorf("There should've been no entries left to delete, got %d", deleted)
	}
}

// TestStore_InvalidTransaction tests what happens if an invalid transaction is passed as parameter
func TestStore_InvalidTransaction(t *testing.T) {
	store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InvalidTransaction.db", false)
	defer store.Close()
	tx, _ := store.db.Begin()
	tx.Commit()
	if _, err := store.insertEndpoint(tx, &testEndpoint); err == nil {
		t.Error("should've returned an error, because the transaction was already committed")
	}
	if err := store.insertEndpointEvent(tx, 1, endpoint.NewEventFromResult(&testSuccessfulResult)); err == nil {
		t.Error("should've returned an error, because the transaction was already committed")
	}
	if err := store.insertEndpointResult(tx, 1, &testSuccessfulResult); err == nil {
		t.Error("should've returned an error, because the transaction was already committed")
	}
	if err := store.insertConditionResults(tx, 1, testSuccessfulResult.ConditionResults); err == nil {
		t.Error("should've returned an error, because the transaction was already committed")
	}
	if err := store.updateEndpointUptime(tx, 1, &testSuccessfulResult); err == nil {
		t.Error("should've returned an error, because the transaction was already committed")
	}
	if _, err := store.getAllEndpointKeys(tx); err == nil {
		t.Error("should've returned an error, because the transaction was already committed")
	}
	if _, err := store.getEndpointStatusByKey(tx, testEndpoint.Key(), paging.NewEndpointStatusParams().WithResults(1, 20)); err == nil {
		t.Error("should've returned an error, because the transaction was already committed")
	}
	if _, err := store.getEndpointEventsByEndpointID(tx, 1, 1, 50); err == nil {
		t.Error("should've returned an error, because the transaction was already committed")
	}
	if _, err := store.getEndpointResultsByEndpointID(tx, 1, 1, 50); err == nil {
		t.Error("should've returned an error, because the transaction was already committed")
	}
	if err := store.deleteOldEndpointEvents(tx, 1); err == nil {
		t.Error("should've returned an error, because the transaction was already committed")
	}
	if err := store.deleteOldEndpointResults(tx, 1); err == nil {
		t.Error("should've returned an error, because the transaction was already committed")
	}
	if _, _, err := store.getEndpointUptime(tx, 1, time.Now(), time.Now()); err == nil {
		t.Error("should've returned an error, because the transaction was already committed")
	}
	if _, err := store.getEndpointID(tx, &testEndpoint); err == nil {
		t.Error("should've returned an error, because the transaction was already committed")
	}
	if _, err := store.getNumberOfEventsByEndpointID(tx, 1); err == nil {
		t.Error("should've returned an error, because the transaction was already committed")
	}
	if _, err := store.getNumberOfResultsByEndpointID(tx, 1); err == nil {
		t.Error("should've returned an error, because the transaction was already committed")
	}
	if _, err := store.getAgeOfOldestEndpointUptimeEntry(tx, 1); err == nil {
		t.Error("should've returned an error, because the transaction was already committed")
	}
	if _, err := store.getLastEndpointResultSuccessValue(tx, 1); err == nil {
		t.Error("should've returned an error, because the transaction was already committed")
	}
}

func TestStore_NoRows(t *testing.T) {
	store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_NoRows.db", false)
	defer store.Close()
	tx, _ := store.db.Begin()
	defer tx.Rollback()
	if _, err := store.getLastEndpointResultSuccessValue(tx, 1); !errors.Is(err, errNoRowsReturned) {
		t.Errorf("should've %v, got %v", errNoRowsReturned, err)
	}
	if _, err := store.getAgeOfOldestEndpointUptimeEntry(tx, 1); !errors.Is(err, errNoRowsReturned) {
		t.Errorf("should've %v, got %v", errNoRowsReturned, err)
	}
}

// This tests very unlikely cases where a table is deleted.
func TestStore_BrokenSchema(t *testing.T) {
	store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_BrokenSchema.db", false)
	defer store.Close()
	if err := store.Insert(&testEndpoint, &testSuccessfulResult); err != nil {
		t.Fatal("expected no error, got", err.Error())
	}
	if _, err := store.GetAverageResponseTimeByKey(testEndpoint.Key(), time.Now().Add(-time.Hour), time.Now()); err != nil {
		t.Fatal("expected no error, got", err.Error())
	}
	if _, err := store.GetAllEndpointStatuses(paging.NewEndpointStatusParams()); err != nil {
		t.Fatal("expected no error, got", err.Error())
	}
	// Break
	_, _ = store.db.Exec("DROP TABLE endpoints")
	// And now we'll try to insert something in our broken schema
	if err := store.Insert(&testEndpoint, &testSuccessfulResult); err == nil {
		t.Fatal("expected an error")
	}
	if _, err := store.GetAverageResponseTimeByKey(testEndpoint.Key(), time.Now().Add(-time.Hour), time.Now()); err == nil {
		t.Fatal("expected an error")
	}
	if _, err := store.GetHourlyAverageResponseTimeByKey(testEndpoint.Key(), time.Now().Add(-time.Hour), time.Now()); err == nil {
		t.Fatal("expected an error")
	}
	if _, err := store.GetAllEndpointStatuses(paging.NewEndpointStatusParams()); err == nil {
		t.Fatal("expected an error")
	}
	if _, err := store.GetUptimeByKey(testEndpoint.Key(), time.Now().Add(-time.Hour), time.Now()); err == nil {
		t.Fatal("expected an error")
	}
	if _, err := store.GetEndpointStatusByKey(testEndpoint.Key(), paging.NewEndpointStatusParams()); err == nil {
		t.Fatal("expected an error")
	}
	// Repair
	if err := store.createSchema(); err != nil {
		t.Fatal("schema should've been repaired")
	}
	store.Clear()
	if err := store.Insert(&testEndpoint, &testSuccessfulResult); err != nil {
		t.Fatal("expected no error, got", err.Error())
	}
	// Break
	_, _ = store.db.Exec("DROP TABLE endpoint_events")
	if err := store.Insert(&testEndpoint, &testSuccessfulResult); err != nil {
		t.Fatal("expected no error, because this should silently fails, got", err.Error())
	}
	if _, err := store.GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(1, 1).WithEvents(1, 1)); err != nil {
		t.Fatal("expected no error, because this should silently fail, got", err.Error())
	}
	// Repair
	if err := store.createSchema(); err != nil {
		t.Fatal("schema should've been repaired")
	}
	store.Clear()
	if err := store.Insert(&testEndpoint, &testSuccessfulResult); err != nil {
		t.Fatal("expected no error, got", err.Error())
	}
	// Break
	_, _ = store.db.Exec("DROP TABLE endpoint_results")
	if err := store.Insert(&testEndpoint, &testSuccessfulResult); err == nil {
		t.Fatal("expected an error")
	}
	if _, err := store.GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(1, 1).WithEvents(1, 1)); err != nil {
		t.Fatal("expected no error, because this should silently fail, got", err.Error())
	}
	// Repair
	if err := store.createSchema(); err != nil {
		t.Fatal("schema should've been repaired")
	}
	store.Clear()
	if err := store.Insert(&testEndpoint, &testSuccessfulResult); err != nil {
		t.Fatal("expected no error, got", err.Error())
	}
	// Break
	_, _ = store.db.Exec("DROP TABLE endpoint_result_conditions")
	if err := store.Insert(&testEndpoint, &testSuccessfulResult); err == nil {
		t.Fatal("expected an error")
	}
	// Repair
	if err := store.createSchema(); err != nil {
		t.Fatal("schema should've been repaired")
	}
	store.Clear()
	if err := store.Insert(&testEndpoint, &testSuccessfulResult); err != nil {
		t.Fatal("expected no error, got", err.Error())
	}
	// Break
	_, _ = store.db.Exec("DROP TABLE endpoint_uptimes")
	if err := store.Insert(&testEndpoint, &testSuccessfulResult); err != nil {
		t.Fatal("expected no error, because this should silently fails, got", err.Error())
	}
	if _, err := store.GetAverageResponseTimeByKey(testEndpoint.Key(), time.Now().Add(-time.Hour), time.Now()); err == nil {
		t.Fatal("expected an error")
	}
	if _, err := store.GetHourlyAverageResponseTimeByKey(testEndpoint.Key(), time.Now().Add(-time.Hour), time.Now()); err == nil {
		t.Fatal("expected an error")
	}
	if _, err := store.GetUptimeByKey(testEndpoint.Key(), time.Now().Add(-time.Hour), time.Now()); err == nil {
		t.Fatal("expected an error")
	}
}

func TestCacheKey(t *testing.T) {
	scenarios := []struct {
		endpointKey      string
		params           paging.EndpointStatusParams
		overrideCacheKey string
		expectedCacheKey string
		wantErr          bool
	}{
		{
			endpointKey:      "simple",
			params:           paging.EndpointStatusParams{EventsPage: 1, EventsPageSize: 2, ResultsPage: 3, ResultsPageSize: 4},
			expectedCacheKey: "simple-1-2-3-4",
			wantErr:          false,
		},
		{
			endpointKey:      "with-hyphen",
			params:           paging.EndpointStatusParams{EventsPage: 0, EventsPageSize: 0, ResultsPage: 1, ResultsPageSize: 20},
			expectedCacheKey: "with-hyphen-0-0-1-20",
			wantErr:          false,
		},
		{
			endpointKey:      "with-multiple-hyphens",
			params:           paging.EndpointStatusParams{EventsPage: 0, EventsPageSize: 0, ResultsPage: 2, ResultsPageSize: 20},
			expectedCacheKey: "with-multiple-hyphens-0-0-2-20",
			wantErr:          false,
		},
		{
			overrideCacheKey: "invalid-a-2-3-4",
			wantErr:          true,
		},
		{
			overrideCacheKey: "invalid-1-a-3-4",
			wantErr:          true,
		},
		{
			overrideCacheKey: "invalid-1-2-a-4",
			wantErr:          true,
		},
		{
			overrideCacheKey: "invalid-1-2-3-a",
			wantErr:          true,
		},
		{
			overrideCacheKey: "notenoughhyphen1-2-3-4",
			wantErr:          true,
		},
	}
	for _, scenario := range scenarios {
		t.Run(scenario.expectedCacheKey+scenario.overrideCacheKey, func(t *testing.T) {
			var cacheKey string
			if len(scenario.overrideCacheKey) > 0 {
				cacheKey = scenario.overrideCacheKey
			} else {
				cacheKey = generateCacheKey(scenario.endpointKey, &scenario.params)
				if cacheKey != scenario.expectedCacheKey {
					t.Errorf("expected %s, got %s", scenario.expectedCacheKey, cacheKey)
				}
			}
			extractedEndpointKey, extractedParams, err := extractKeyAndParamsFromCacheKey(cacheKey)
			if (err != nil) != scenario.wantErr {
				t.Errorf("expected error %v, got %v", scenario.wantErr, err)
				return
			}
			if err != nil {
				// If there's an error, we don't need to check the extracted values
				return
			}
			if extractedEndpointKey != scenario.endpointKey {
				t.Errorf("expected endpointKey %s, got %s", scenario.endpointKey, extractedEndpointKey)
			}
			if extractedParams.EventsPage != scenario.params.EventsPage {
				t.Errorf("expected EventsPage %d, got %d", scenario.params.EventsPage, extractedParams.EventsPage)
			}
			if extractedParams.EventsPageSize != scenario.params.EventsPageSize {
				t.Errorf("expected EventsPageSize %d, got %d", scenario.params.EventsPageSize, extractedParams.EventsPageSize)
			}
			if extractedParams.ResultsPage != scenario.params.ResultsPage {
				t.Errorf("expected ResultsPage %d, got %d", scenario.params.ResultsPage, extractedParams.ResultsPage)
			}
			if extractedParams.ResultsPageSize != scenario.params.ResultsPageSize {
				t.Errorf("expected ResultsPageSize %d, got %d", scenario.params.ResultsPageSize, extractedParams.ResultsPageSize)
			}
		})
	}
}

func TestTriggeredEndpointAlertsPersistence(t *testing.T) {
	store, _ := NewStore("sqlite", t.TempDir()+"/TestTriggeredEndpointAlertsPersistence.db", false)
	defer store.Close()
	yes, desc := false, "description"
	ep := testEndpoint
	ep.NumberOfSuccessesInARow = 0
	alrt := &alert.Alert{
		Type:             alert.TypePagerDuty,
		Enabled:          &yes,
		FailureThreshold: 4,
		SuccessThreshold: 2,
		Description:      &desc,
		SendOnResolved:   &yes,
		Triggered:        true,
		ResolveKey:       "1234567",
	}
	// Alert just triggered, so NumberOfSuccessesInARow is 0
	if err := store.UpsertTriggeredEndpointAlert(&ep, alrt); err != nil {
		t.Fatal("expected no error, got", err.Error())
	}
	exists, resolveKey, numberOfSuccessesInARow, err := store.GetTriggeredEndpointAlert(&ep, alrt)
	if err != nil {
		t.Fatal("expected no error, got", err.Error())
	}
	if !exists {
		t.Error("expected triggered alert to exist")
	}
	if resolveKey != alrt.ResolveKey {
		t.Errorf("expected resolveKey %s, got %s", alrt.ResolveKey, resolveKey)
	}
	if numberOfSuccessesInARow != ep.NumberOfSuccessesInARow {
		t.Errorf("expected persisted NumberOfSuccessesInARow to be %d, got %d", ep.NumberOfSuccessesInARow, numberOfSuccessesInARow)
	}
	// Endpoint just had a successful evaluation, so NumberOfSuccessesInARow is now 1
	ep.NumberOfSuccessesInARow++
	if err := store.UpsertTriggeredEndpointAlert(&ep, alrt); err != nil {
		t.Fatal("expected no error, got", err.Error())
	}
	exists, resolveKey, numberOfSuccessesInARow, err = store.GetTriggeredEndpointAlert(&ep, alrt)
	if err != nil {
		t.Error("expected no error, got", err.Error())
	}
	if !exists {
		t.Error("expected triggered alert to exist")
	}
	if resolveKey != alrt.ResolveKey {
		t.Errorf("expected resolveKey %s, got %s", alrt.ResolveKey, resolveKey)
	}
	if numberOfSuccessesInARow != ep.NumberOfSuccessesInARow {
		t.Errorf("expected persisted NumberOfSuccessesInARow to be %d, got %d", ep.NumberOfSuccessesInARow, numberOfSuccessesInARow)
	}
	// Simulate the endpoint having another successful evaluation, which means the alert is now resolved,
	// and we should delete the triggered alert from the store
	ep.NumberOfSuccessesInARow++
	if err := store.DeleteTriggeredEndpointAlert(&ep, alrt); err != nil {
		t.Fatal("expected no error, got", err.Error())
	}
	exists, _, _, err = store.GetTriggeredEndpointAlert(&ep, alrt)
	if err != nil {
		t.Error("expected no error, got", err.Error())
	}
	if exists {
		t.Error("expected triggered alert to no longer exist as it has been deleted")
	}
}

func TestStore_DeleteAllTriggeredAlertsNotInChecksumsByEndpoint(t *testing.T) {
	store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_DeleteAllTriggeredAlertsNotInChecksumsByEndpoint.db", false)
	defer store.Close()
	yes, desc := false, "description"
	ep1 := testEndpoint
	ep1.Name = "ep1"
	ep2 := testEndpoint
	ep2.Name = "ep2"
	alert1 := alert.Alert{
		Type:             alert.TypePagerDuty,
		Enabled:          &yes,
		FailureThreshold: 4,
		SuccessThreshold: 2,
		Description:      &desc,
		SendOnResolved:   &yes,
		Triggered:        true,
		ResolveKey:       "1234567",
	}
	alert2 := alert1
	alert2.Type, alert2.ResolveKey = alert.TypeSlack, ""
	alert3 := alert2
	if err := store.UpsertTriggeredEndpointAlert(&ep1, &alert1); err != nil {
		t.Fatal("expected no error, got", err.Error())
	}
	if err := store.UpsertTriggeredEndpointAlert(&ep1, &alert2); err != nil {
		t.Fatal("expected no error, got", err.Error())
	}
	if err := store.UpsertTriggeredEndpointAlert(&ep2, &alert3); err != nil {
		t.Fatal("expected no error, got", err.Error())
	}
	if exists, _, _, _ := store.GetTriggeredEndpointAlert(&ep1, &alert1); !exists {
		t.Error("expected alert1 to have been deleted")
	}
	if exists, _, _, _ := store.GetTriggeredEndpointAlert(&ep1, &alert2); !exists {
		t.Error("expected alert2 to exist for ep1")
	}
	if exists, _, _, _ := store.GetTriggeredEndpointAlert(&ep2, &alert3); !exists {
		t.Error("expected alert3 to exist for ep2")
	}
	// Now we simulate the alert configuration being updated, and the alert being resolved
	if deleted := store.DeleteAllTriggeredAlertsNotInChecksumsByEndpoint(&ep1, []string{alert2.Checksum()}); deleted != 1 {
		t.Errorf("expected 1 triggered alert to be deleted, got %d", deleted)
	}
	if exists, _, _, _ := store.GetTriggeredEndpointAlert(&ep1, &alert1); exists {
		t.Error("expected alert1 to have been deleted")
	}
	if exists, _, _, _ := store.GetTriggeredEndpointAlert(&ep1, &alert2); !exists {
		t.Error("expected alert2 to exist for ep1")
	}
	if exists, _, _, _ := store.GetTriggeredEndpointAlert(&ep2, &alert3); !exists {
		t.Error("expected alert3 to exist for ep2")
	}
	// Now let's just assume all alerts for ep1 were removed
	if deleted := store.DeleteAllTriggeredAlertsNotInChecksumsByEndpoint(&ep1, []string{}); deleted != 1 {
		t.Errorf("expected 1 triggered alert to be deleted, got %d", deleted)
	}
	// Make sure the alert for ep2 still exists
	if exists, _, _, _ := store.GetTriggeredEndpointAlert(&ep2, &alert3); !exists {
		t.Error("expected alert3 to exist for ep2")
	}
}