package control

import (
	"context"
	"encoding/json"
	"github.com/gin-gonic/gin"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"google.golang.org/api/googleapi"
	loggingapi "google.golang.org/api/logging/v2"
	"google.golang.org/api/option"
)

const testSchedulerJob = "projects/test-project/locations/asia-east1/jobs/finance-daily"

func schedulerEntry(phase, status string, at time.Time) *loggingapi.LogEntry {
	body, _ := json.Marshal(map[string]string{"@type": "type.googleapis.com/google.cloud.scheduler.logging.Attempt" + phase, "jobName": testSchedulerJob, "status": status})
	return &loggingapi.LogEntry{InsertId: phase, LogName: "projects/test-project/logs/cloudscheduler.googleapis.com%2Fexecutions", Timestamp: at.Format(time.RFC3339Nano), Resource: &loggingapi.MonitoredResource{Type: "cloud_scheduler_job", Labels: map[string]string{"project_id": "test-project", "location": "asia-east1", "job_id": "finance-daily"}}, JsonPayload: googleapi.RawMessage(body)}
}

func TestSchedulerLogEvidenceIsIndependentAndSanitized(t *testing.T) {
	now := time.Now().UTC()
	start, err := schedulerLogTrigger(schedulerEntry("Started", "", now), testSchedulerJob, now)
	if err != nil || start.Status != "dispatch_started" || start.RunID != "" {
		t.Fatal("start is not proof of execution", err)
	}
	finish, err := schedulerLogTrigger(schedulerEntry("Finished", "INTERNAL", now.Add(time.Second)), testSchedulerJob, now)
	if err != nil || finish.Status != "failed" || finish.ID == start.ID || finish.RunID != "" {
		t.Fatal("time adjacency fabricated attempt/run correlation", err)
	}
	for _, status := range []string{"", "NEW_PROVIDER_STATUS"} {
		unknown, err := schedulerLogTrigger(schedulerEntry("Finished", status, now), testSchedulerJob, now)
		if err != nil || unknown.Status != "unknown" {
			t.Fatal("missing/novel status treated as success")
		}
	}
	good, err := schedulerLogTrigger(schedulerEntry("Finished", "OK", now), testSchedulerJob, now)
	if err != nil || good.Status != "accepted" {
		t.Fatal("valid response not recorded")
	}
	entry := schedulerEntry("Finished", "INTERNAL", now)
	entry.JsonPayload = googleapi.RawMessage(`{"@type":"type.googleapis.com/google.cloud.scheduler.logging.AttemptFinished","jobName":"` + testSchedulerJob + `","status":"INTERNAL","debugInfo":"private payload Original HTTP response code number = 500","url":"https://private.test?token=secret"}`)
	failure, err := schedulerLogTrigger(entry, testSchedulerJob, now)
	if err != nil || failure.HTTPStatus != 500 {
		t.Fatal(err)
	}
	encoded, _ := json.Marshal(failure)
	if strings.Contains(string(encoded), "private") || strings.Contains(string(encoded), "secret") {
		t.Fatal("raw provider payload leaked")
	}
	entry.Resource.Labels["job_id"] = "another-job"
	if _, err := schedulerLogTrigger(entry, testSchedulerJob, now); err == nil {
		t.Fatal("cross-job scope accepted")
	}
	o := testOperations(t)
	b := RunBatch{Triggers: []TriggerObservation{start, finish}, State: CollectionState{Provider: "finance_scheduler_logs", Status: "partial", ObservedAt: now}}
	for i := 0; i < 2; i++ {
		if err := o.CommitBatch(context.Background(), b); err != nil {
			t.Fatal(err)
		}
	}
	if countTable(t, o, "triggers") != 2 || countTable(t, o, "alerts") != 1 {
		t.Fatal("replayed log creates duplicate trigger or alert")
	}
	// Backfilled older logs must not displace newer events in the UI ordering.
	older, _ := schedulerLogTrigger(schedulerEntry("Started", "", now.Add(-time.Hour)), testSchedulerJob, now)
	b.Triggers = []TriggerObservation{older}
	if err := o.CommitBatch(context.Background(), b); err != nil {
		t.Fatal(err)
	}
	r := gin.New()
	o.Register(r, nil)
	w := httptest.NewRecorder()
	r.ServeHTTP(w, httptest.NewRequest("GET", "/api/v1/triggers", nil))
	var page struct {
		Items []TriggerObservation `json:"items"`
	}
	if err := json.Unmarshal(w.Body.Bytes(), &page); err != nil || len(page.Items) != 3 || page.Items[0].ID != finish.ID {
		t.Fatal("trigger list ordered by backfill arrival, not event time", err)
	}
}

func TestSchedulerLogsStablePaginationAndEmptyTokenPage(t *testing.T) {
	now := time.Now().UTC().Add(-time.Minute)
	var firstFilter string
	calls := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != "POST" || r.URL.Path != "/v2/entries:list" {
			t.Errorf("unexpected log API request %s %s", r.Method, r.URL.Path)
		}
		var request loggingapi.ListLogEntriesRequest
		if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
			t.Error(err)
		}
		if len(request.ResourceNames) != 1 || request.ResourceNames[0] != "projects/test-project" || request.PageSize != 200 || request.OrderBy != "timestamp desc" || !strings.Contains(request.Filter, `resource.labels.job_id="finance-daily"`) {
			t.Error("unbounded or wrong log scope")
		}
		calls++
		w.Header().Set("Content-Type", "application/json")
		if calls == 1 {
			firstFilter = request.Filter
			_ = json.NewEncoder(w).Encode(loggingapi.ListLogEntriesResponse{NextPageToken: "next-page"})
		} else {
			if request.PageToken != "next-page" || request.Filter != firstFilter {
				t.Error("pagination changed snapshot/filter")
			}
			_ = json.NewEncoder(w).Encode(loggingapi.ListLogEntriesResponse{Entries: []*loggingapi.LogEntry{schedulerEntry("Finished", "INTERNAL", now)}})
		}
	}))
	defer server.Close()
	client, err := loggingapi.NewService(context.Background(), option.WithEndpoint(server.URL+"/"), option.WithoutAuthentication())
	if err != nil {
		t.Fatal(err)
	}
	s := &ExternalLiveSource{config: LiveSourceConfig{SchedulerJobName: testSchedulerJob}, logging: client}
	first, err := s.schedulerLogsHistory(context.Background(), CollectionState{})
	if err != nil || first.State.Cursor == "" || len(first.Triggers) != 0 {
		t.Fatal("empty page with token incorrectly finished", err)
	}
	second, err := s.schedulerLogsHistory(context.Background(), first.State)
	if err != nil || second.State.Cursor != "" || len(second.Triggers) != 1 || !second.State.CoveredFrom.Equal(first.State.CoveredFrom) {
		t.Fatal("pagination lost snapshot or evidence", err)
	}
}

func TestSchedulerLogsRejectedTokenResetsWithoutDiscardingEvidence(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(400)
		_, _ = w.Write([]byte(`{"error":{"code":400,"message":"expired page token"}}`))
	}))
	defer server.Close()
	client, err := loggingapi.NewService(context.Background(), option.WithEndpoint(server.URL+"/"), option.WithoutAuthentication())
	if err != nil {
		t.Fatal(err)
	}
	s := &ExternalLiveSource{config: LiveSourceConfig{SchedulerJobName: testSchedulerJob}, logging: client}
	now := time.Now().UTC()
	cursor, _ := json.Marshal(schedulerLogCursor{From: now.Add(-time.Hour), Until: now, PageToken: "expired"})
	b, err := s.schedulerLogsHistory(context.Background(), CollectionState{Cursor: string(cursor)})
	if err != nil || b.State.Status != "unavailable" || b.State.Cursor != "" {
		t.Fatal("expired cursor would loop forever", err)
	}
}

func TestSchedulerObservationTimeAndFailureAreTruthful(t *testing.T) {
	now := time.Now().UTC()
	next := now.AddDate(0, 1, 0)
	last := now.Add(-time.Hour)
	fact := SchedulerFact{State: "ENABLED", HasExplicitBody: true, LastStatusCode: 13, NextRunAt: &next, LastAttemptAt: &last}
	dep := schedulerDependency(fact, now)
	if dep.Status != "degraded" || dep.ObservedAt == nil || !dep.ObservedAt.Equal(now) {
		t.Fatal("failed dispatch called healthy or future time used as observation")
	}
}

func TestCollectionAPIKeepsProviderPageTokensInternal(t *testing.T) {
	o := testOperations(t)
	b := RunBatch{State: CollectionState{Provider: "finance_scheduler_logs", Status: "partial", Cursor: "opaque-provider-token", ObservedAt: time.Now().UTC()}}
	if err := o.CommitBatch(context.Background(), b); err != nil {
		t.Fatal(err)
	}
	r := gin.New()
	o.Register(r, nil)
	w := httptest.NewRecorder()
	r.ServeHTTP(w, httptest.NewRequest("GET", "/api/v1/collection-status", nil))
	if w.Code != 200 || strings.Contains(w.Body.String(), "opaque-provider-token") || strings.Contains(w.Body.String(), "cursor") {
		t.Fatal("provider cursor exposed to UI")
	}
	if o.Checkpoint(context.Background(), "finance_scheduler_logs").Cursor != "opaque-provider-token" {
		t.Fatal("internal checkpoint was removed")
	}
}
