package main

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"
)

func TestParseClusterCSV(t *testing.T) {
	clusters, err := parseClusterCSV(strings.NewReader(
		`"serverless_cluster_id"
"10543958977267765697"
"10614256606609258380"
"10543958977267765697"
`,
	))
	if err != nil {
		t.Fatalf("parseClusterCSV() error = %v", err)
	}
	want := []string{"10543958977267765697", "10614256606609258380"}
	if len(clusters) != len(want) {
		t.Fatalf("clusters = %v, want %v", clusters, want)
	}
	for i := range want {
		if clusters[i] != want[i] {
			t.Fatalf("clusters = %v, want %v", clusters, want)
		}
	}
}

func TestParseClusterCSVMissingColumn(t *testing.T) {
	_, err := parseClusterCSV(strings.NewReader("not_cluster_id\nvalue\n"))
	if err == nil {
		t.Fatal("parseClusterCSV() error = nil, want missing-column error")
	}
}

func TestAuthenticateAndQueryExport(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		switch request.URL.Path {
		case "/auth":
			var body map[string]string
			if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
				t.Fatalf("decode auth request: %v", err)
			}
			if body["github_token"] != "github-token" || body["provider"] != "aws" {
				t.Fatalf("auth body = %#v", body)
			}
			_, _ = writer.Write([]byte(`{"token":"o11y-token"}`))
		case "/olap":
			if got := request.Header.Get("o11ytoken"); got != "o11y-token" {
				t.Fatalf("o11ytoken = %q", got)
			}
			if got := request.Header.Get("x-region"); got != "us-east-1" {
				t.Fatalf("x-region = %q", got)
			}
			var body map[string]any
			if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
				t.Fatalf("decode OLAP request: %v", err)
			}
			sql, _ := body["sql"].(string)
			if !strings.Contains(sql, `sqlstatement_org_1372813089209260919_all`) ||
				!strings.Contains(sql, `"date" >= '2026-07-16'`) ||
				!strings.Contains(sql, `- 600`) {
				t.Fatalf("unexpected SQL: %s", sql)
			}
			_, _ = writer.Write([]byte(`{
				"status":"ok",
				"data":{"status":"SUCCEEDED","result":{"status":"SUCCEEDED","download_urls":[{"url":"https://download.example/result.csv"}]}}
			}`))
		default:
			http.NotFound(writer, request)
		}
	}))
	defer server.Close()

	client := O11YClient{
		httpClient: server.Client(),
		config: Config{
			GitHubToken: "github-token",
			AuthURL:     server.URL + "/auth",
			OLAPURL:     server.URL + "/olap",
			Provider:    "aws",
			Region:      "us-east-1",
			QueryWindow: 10 * time.Minute,
		},
	}
	token, err := client.authenticate(context.Background())
	if err != nil {
		t.Fatalf("authenticate() error = %v", err)
	}
	urls, err := client.queryExport(context.Background(), token, time.Date(2026, 7, 16, 12, 0, 0, 0, time.UTC))
	if err != nil {
		t.Fatalf("queryExport() error = %v", err)
	}
	if len(urls) != 1 || urls[0] != "https://download.example/result.csv" {
		t.Fatalf("download URLs = %v", urls)
	}
}

func TestQueryExportRejectsUnsuccessfulResponse(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		_, _ = writer.Write([]byte(`{"status":"ok","data":{"status":"RUNNING","result":{"status":"RUNNING"}}}`))
	}))
	defer server.Close()

	client := O11YClient{
		httpClient: server.Client(),
		config: Config{
			OLAPURL:     server.URL,
			Provider:    "aws",
			Region:      "us-east-1",
			QueryWindow: time.Minute,
		},
	}
	_, err := client.queryExport(context.Background(), "token", time.Now())
	if err == nil {
		t.Fatal("queryExport() error = nil, want unsuccessful export error")
	}
}

func TestNotifyFailure(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		if request.Method != http.MethodPost {
			t.Fatalf("method = %s", request.Method)
		}
		var payload struct {
			MsgType string `json:"msg_type"`
			Content struct {
				Text string `json:"text"`
			} `json:"content"`
		}
		if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
			t.Fatalf("decode request: %v", err)
		}
		if payload.MsgType != "text" || !strings.Contains(payload.Content.Text, "database unavailable") {
			t.Fatalf("unexpected payload: %#v", payload)
		}
		writer.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	notifier := Notifier{webhook: server.URL, httpClient: server.Client()}
	if err := notifier.notifyFailure(context.Background(), errors.New("database unavailable")); err != nil {
		t.Fatalf("notifyFailure() error = %v", err)
	}
}

func TestBuildUpsertBatch(t *testing.T) {
	query, args := buildUpsertBatch([]string{"cluster-a", "cluster-b"})
	if !strings.Contains(query, "INSERT INTO manus.active_cluster_ids") {
		t.Fatalf("query targets wrong table: %s", query)
	}
	if !strings.Contains(query, "(cluster_id, created_at, updated_at, update_count)") {
		t.Fatalf("query lacks update_count insert column: %s", query)
	}
	if strings.Count(query, "CURRENT_TIMESTAMP(6)") != 5 {
		t.Fatalf("query has unexpected timestamps: %s", query)
	}
	if !strings.Contains(query, "ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP(6), update_count = update_count + 1") {
		t.Fatalf("query lacks update clause: %s", query)
	}
	if len(args) != 2 || args[0] != "cluster-a" || args[1] != "cluster-b" {
		t.Fatalf("args = %#v", args)
	}
}

func TestIsMissedRun(t *testing.T) {
	startedAt := time.Date(2026, 7, 16, 14, 0, 0, 0, time.UTC)
	if isMissedRun(startedAt, startedAt.Add(missedRunThreshold-time.Nanosecond)) {
		t.Fatal("run below the missed-run threshold was marked missed")
	}
	if !isMissedRun(startedAt, startedAt.Add(missedRunThreshold)) {
		t.Fatal("run at the missed-run threshold was not marked missed")
	}
}

func TestNextScheduledRun(t *testing.T) {
	tests := []struct {
		name  string
		after time.Time
		want  time.Time
	}{
		{
			name:  "rounds up to first five past",
			after: time.Date(2026, 7, 20, 12, 0, 0, 0, time.UTC),
			want:  time.Date(2026, 7, 20, 12, 5, 0, 0, time.UTC),
		},
		{
			name:  "moves exact scheduled minute to following slot",
			after: time.Date(2026, 7, 20, 12, 5, 0, 0, time.UTC),
			want:  time.Date(2026, 7, 20, 12, 15, 0, 0, time.UTC),
		},
		{
			name:  "moves to next slot within hour",
			after: time.Date(2026, 7, 20, 12, 6, 30, 0, time.UTC),
			want:  time.Date(2026, 7, 20, 12, 15, 0, 0, time.UTC),
		},
		{
			name:  "wraps to next hour",
			after: time.Date(2026, 7, 20, 12, 56, 1, 0, time.UTC),
			want:  time.Date(2026, 7, 20, 13, 5, 0, 0, time.UTC),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := nextScheduledRun(tt.after)
			if !got.Equal(tt.want) {
				t.Fatalf("nextScheduledRun(%s) = %s, want %s", tt.after, got, tt.want)
			}
		})
	}
}
