package scheduler

import (
	"bytes"
	"encoding/json"
	"strings"
	"testing"
	"time"

	"git.toki-labs.com/toki/alt/packages/domain/market"
)

func TestDecideBackfillFreshState(t *testing.T) {
	obs := FreshnessObservation{
		Schedule:       "daily-daily",
		Provider:       market.ProviderKIS,
		Selector:       "kospi",
		Timeframe:      "daily",
		Timezone:       "Asia/Seoul",
		Cadence:        "24h0m0s",
		BackfillWindow: "7d",
		Status:         "success",
	}

	window := 7 * 24 * time.Hour
	dec := DecideBackfill(obs, window)

	if dec.Status != "fresh" {
		t.Errorf("expected status=fresh, got %s", dec.Status)
	}
	if dec.HasBackfill {
		t.Error("expected has_backfill=false for fresh state")
	}
	if dec.Retry {
		t.Error("expected retry=false for fresh state")
	}
	if dec.Reason != "all expected dates are present" {
		t.Errorf("unexpected reason: %s", dec.Reason)
	}
}

func TestDecideBackfillMissingDates(t *testing.T) {
	missingDates := []time.Time{
		time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC),
		time.Date(2026, 6, 16, 0, 0, 0, 0, time.UTC),
		time.Date(2026, 6, 18, 0, 0, 0, 0, time.UTC),
	}
	obs := FreshnessObservation{
		Schedule:       "daily-daily",
		Provider:       market.ProviderKIS,
		Selector:       "kospi",
		Timeframe:      "daily",
		Timezone:       "Asia/Seoul",
		Cadence:        "24h0m0s",
		BackfillWindow: "7d",
		Status:         "success",
		MissingDates:   missingDates,
		ExpectedTo:     time.Date(2026, 6, 20, 0, 0, 0, 0, time.UTC),
	}

	window := 7 * 24 * time.Hour
	dec := DecideBackfill(obs, window)

	if dec.Status != "stale" {
		t.Errorf("expected status=stale for missing dates, got %s", dec.Status)
	}
	if !dec.HasBackfill {
		t.Error("expected has_backfill=true for missing dates")
	}
	if !dec.Retry {
		t.Error("expected retry=true for missing dates")
	}
	if dec.MissingCount != 3 {
		t.Errorf("expected missing_count=3, got %d", dec.MissingCount)
	}
	if dec.BackfillFrom.IsZero() {
		t.Error("expected backfill_from to be set")
	}
	// The earliest missing date is 2026-06-15
	if !dec.BackfillFrom.Equal(time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC)) {
		t.Errorf("unexpected backfill_from: %s", dec.BackfillFrom.Format("2006-01-02"))
	}
	if !strings.Contains(dec.Reason, "3 missing date(s)") {
		t.Errorf("unexpected reason: %s", dec.Reason)
	}
}

func TestDecideBackfillGapBuckets(t *testing.T) {
	gapBuckets := []GapBucket{
		{From: time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC), To: time.Date(2026, 6, 16, 0, 0, 0, 0, time.UTC)},
		{From: time.Date(2026, 6, 18, 0, 0, 0, 0, time.UTC), To: time.Date(2026, 6, 18, 0, 0, 0, 0, time.UTC)},
	}
	obs := FreshnessObservation{
		Schedule:       "daily-daily",
		Provider:       market.ProviderKIS,
		Selector:       "kospi",
		Timeframe:      "daily",
		Timezone:       "Asia/Seoul",
		Cadence:        "24h0m0s",
		BackfillWindow: "7d",
		Status:         "success",
		GapBuckets:     gapBuckets,
		ExpectedTo:     time.Date(2026, 6, 20, 0, 0, 0, 0, time.UTC),
		ExpectedFrom:   time.Date(2026, 6, 13, 0, 0, 0, 0, time.UTC),
	}

	window := 7 * 24 * time.Hour
	dec := DecideBackfill(obs, window)

	if dec.Status != "stale" {
		t.Errorf("expected status=stale for gap buckets, got %s", dec.Status)
	}
	if !dec.HasBackfill {
		t.Error("expected has_backfill=true for gap buckets")
	}
	if dec.GapCount != 2 {
		t.Errorf("expected gap_count=2, got %d", dec.GapCount)
	}
	if !strings.Contains(dec.Reason, "2 gap bucket(s)") {
		t.Errorf("unexpected reason: %s", dec.Reason)
	}
}

func TestDecideBackfillProviderDelayOnly(t *testing.T) {
	obs := FreshnessObservation{
		Schedule:       "daily-daily",
		Provider:       market.ProviderKIS,
		Selector:       "kospi",
		Timeframe:      "daily",
		Timezone:       "Asia/Seoul",
		Cadence:        "24h0m0s",
		BackfillWindow: "7d",
		Status:         "success",
		ProviderDelay:  2,
		ExpectedTo:     time.Date(2026, 6, 20, 0, 0, 0, 0, time.UTC),
	}

	window := 7 * 24 * time.Hour
	dec := DecideBackfill(obs, window)

	if dec.Status != "stale" {
		t.Errorf("expected status=stale for provider delay, got %s", dec.Status)
	}
	if dec.HasBackfill {
		t.Error("expected has_backfill=false for provider delay only")
	}
	if dec.Retry {
		t.Error("expected retry=false for provider delay only")
	}
	if dec.MissingCount != 0 {
		t.Errorf("expected missing_count=0, got %d", dec.MissingCount)
	}
	if dec.ProviderDelay != 2 {
		t.Errorf("expected provider_delay=2, got %d", dec.ProviderDelay)
	}
}

func TestDecideBackfillPreservesErrorState(t *testing.T) {
	obs := FreshnessObservation{
		Schedule:       "daily-daily",
		Provider:       market.ProviderKIS,
		Selector:       "kospi",
		Timeframe:      "daily",
		Timezone:       "Asia/Seoul",
		Cadence:        "24h0m0s",
		BackfillWindow: "7d",
		Status:         "error",
		LastError:      "connection timeout to KIS API",
		ExpectedTo:     time.Date(2026, 6, 20, 0, 0, 0, 0, time.UTC),
	}

	window := 7 * 24 * time.Hour
	dec := DecideBackfill(obs, window)

	if dec.Status != "error" {
		t.Errorf("expected status=error, got %s", dec.Status)
	}
	if dec.HasBackfill {
		t.Error("expected has_backfill=false for error state")
	}
	if !dec.Retry {
		t.Error("expected retry=true for error state")
	}
	if !strings.Contains(dec.Reason, "connection timeout to KIS API") {
		t.Errorf("unexpected reason: %s", dec.Reason)
	}
	if dec.LastError != "connection timeout to KIS API" {
		t.Errorf("unexpected last_error: %s", dec.LastError)
	}
}

func TestDecideBackfillDistinguishesMissingGapProviderDelay(t *testing.T) {
	window := 7 * 24 * time.Hour

	// Case 1: Missing dates only
	obs1 := FreshnessObservation{
		Schedule:     "daily-daily",
		Provider:     market.ProviderKIS,
		Status:       "success",
		MissingDates: []time.Time{time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC)},
		ExpectedTo:   time.Date(2026, 6, 20, 0, 0, 0, 0, time.UTC),
	}
	dec1 := DecideBackfill(obs1, window)
	if dec1.Status != "stale" {
		t.Errorf("case 1 (missing): expected stale, got %s", dec1.Status)
	}
	if !dec1.HasBackfill {
		t.Error("case 1 (missing): expected has_backfill=true")
	}

	// Case 2: Gap buckets only
	obs2 := FreshnessObservation{
		Schedule:     "daily-daily",
		Provider:     market.ProviderKIS,
		Status:       "success",
		GapBuckets:   []GapBucket{{From: time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC), To: time.Date(2026, 6, 16, 0, 0, 0, 0, time.UTC)}},
		ExpectedTo:   time.Date(2026, 6, 20, 0, 0, 0, 0, time.UTC),
		ExpectedFrom: time.Date(2026, 6, 13, 0, 0, 0, 0, time.UTC),
	}
	dec2 := DecideBackfill(obs2, window)
	if dec2.Status != "stale" {
		t.Errorf("case 2 (gap): expected stale, got %s", dec2.Status)
	}
	if !dec2.HasBackfill {
		t.Error("case 2 (gap): expected has_backfill=true")
	}

	// Case 3: Provider delay only
	obs3 := FreshnessObservation{
		Schedule:      "daily-daily",
		Provider:      market.ProviderKIS,
		Status:        "success",
		ProviderDelay: 3,
		ExpectedTo:    time.Date(2026, 6, 20, 0, 0, 0, 0, time.UTC),
	}
	dec3 := DecideBackfill(obs3, window)
	if dec3.Status != "stale" {
		t.Errorf("case 3 (delay): expected stale, got %s", dec3.Status)
	}
	if dec3.HasBackfill {
		t.Error("case 3 (delay): expected has_backfill=false")
	}
	if dec3.Retry {
		t.Error("case 3 (delay): expected retry=false")
	}

	// Case 4: Error
	obs4 := FreshnessObservation{
		Schedule:   "daily-daily",
		Provider:   market.ProviderKIS,
		Status:     "error",
		LastError:  "api timeout",
		ExpectedTo: time.Date(2026, 6, 20, 0, 0, 0, 0, time.UTC),
	}
	dec4 := DecideBackfill(obs4, window)
	if dec4.Status != "error" {
		t.Errorf("case 4 (error): expected error, got %s", dec4.Status)
	}
}

func TestDecideBackfillDuplicateOnly(t *testing.T) {
	obs := FreshnessObservation{
		Schedule:       "daily-daily",
		Provider:       market.ProviderKIS,
		Selector:       "kospi",
		Status:         "success",
		DuplicateCount: 5,
		ExpectedTo:     time.Date(2026, 6, 20, 0, 0, 0, 0, time.UTC),
	}

	window := 7 * 24 * time.Hour
	dec := DecideBackfill(obs, window)

	// Duplicate-only should still be fresh if no missing/gap/error.
	if dec.Status != "fresh" {
		t.Errorf("expected status=fresh for duplicate-only, got %s", dec.Status)
	}
	if dec.HasBackfill {
		t.Error("expected has_backfill=false for duplicate-only")
	}
}

func TestDecideBackfillBackfillWindowCutoff(t *testing.T) {
	// Missing dates older than the backfill window should be clamped.
	missingDates := []time.Time{
		time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC), // older than 7-day window
		time.Date(2026, 6, 10, 0, 0, 0, 0, time.UTC),
	}
	obs := FreshnessObservation{
		Schedule:     "daily-daily",
		Provider:     market.ProviderKIS,
		Selector:     "kospi",
		Status:       "success",
		MissingDates: missingDates,
		ExpectedTo:   time.Date(2026, 6, 20, 0, 0, 0, 0, time.UTC),
	}

	window := 7 * 24 * time.Hour
	dec := DecideBackfill(obs, window)

	if dec.Status != "stale" {
		t.Errorf("expected status=stale, got %s", dec.Status)
	}
	if !dec.HasBackfill {
		t.Error("expected has_backfill=true")
	}
	// Should be clamped to window start (2026-06-13)
	windowStart := time.Date(2026, 6, 13, 0, 0, 0, 0, time.UTC)
	if !dec.BackfillFrom.Equal(windowStart) {
		t.Errorf("expected backfill_from=%s, got %s", windowStart.Format("2006-01-02"), dec.BackfillFrom.Format("2006-01-02"))
	}
}

func TestDecideBackfillNilSlices(t *testing.T) {
	obs := FreshnessObservation{
		Schedule:     "daily-daily",
		Provider:     market.ProviderKIS,
		Status:       "success",
		MissingDates: nil,
		GapBuckets:   nil,
		ExpectedTo:   time.Date(2026, 6, 20, 0, 0, 0, 0, time.UTC),
	}

	window := 7 * 24 * time.Hour
	dec := DecideBackfill(obs, window)

	if dec.Status != "fresh" {
		t.Errorf("expected status=fresh for nil slices, got %s", dec.Status)
	}
}

func TestWriteBackfillTextAndJSONL(t *testing.T) {
	decisions := []BackfillDecision{
		{
			Schedule:    "daily-daily",
			Status:      "fresh",
			Reason:      "all expected dates are present",
			HasBackfill: false,
			Retry:       false,
		},
		{
			Schedule:     "daily-daily",
			Status:       "stale",
			Reason:       "backfill needed: 3 missing date(s) from 2026-06-15 to 2026-06-20",
			HasBackfill:  true,
			Retry:        true,
			BackfillFrom: time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC),
			BackfillTo:   time.Date(2026, 6, 20, 0, 0, 0, 0, time.UTC),
			MissingCount: 3,
			StaleDates: []time.Time{
				time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC),
				time.Date(2026, 6, 16, 0, 0, 0, 0, time.UTC),
				time.Date(2026, 6, 18, 0, 0, 0, 0, time.UTC),
			},
		},
		{
			Schedule:    "daily-daily",
			Status:      "error",
			Reason:      "previous operation failed: connection timeout",
			LastError:   "connection timeout",
			HasBackfill: false,
			Retry:       true,
		},
	}

	// Test text output
	var textBuf bytes.Buffer
	if err := WriteBackfillText(&textBuf, decisions); err != nil {
		t.Fatalf("WriteBackfillText: %v", err)
	}

	text := textBuf.String()
	textLines := strings.Split(strings.TrimSpace(text), "\n")
	if len(textLines) != 3 {
		t.Errorf("expected 3 text lines, got %d", len(textLines))
	}

	// Check line 1 (fresh)
	if !strings.Contains(textLines[0], "status=fresh") {
		t.Errorf("line 1 should contain status=fresh: %s", textLines[0])
	}
	if !strings.Contains(textLines[0], "backfill_from=NONE") {
		t.Errorf("line 1 should contain backfill_from=NONE: %s", textLines[0])
	}

	// Check line 2 (stale)
	if !strings.Contains(textLines[1], "status=stale") {
		t.Errorf("line 2 should contain status=stale: %s", textLines[1])
	}
	if !strings.Contains(textLines[1], "backfill_from=20260615") {
		t.Errorf("line 2 should contain backfill_from=20260615: %s", textLines[1])
	}
	if !strings.Contains(textLines[1], "backfill_to=20260620") {
		t.Errorf("line 2 should contain backfill_to=20260620: %s", textLines[1])
	}

	// Check line 3 (error)
	if !strings.Contains(textLines[2], "status=error") {
		t.Errorf("line 3 should contain status=error: %s", textLines[2])
	}

	// Test JSONL output (single-line JSON per decision, no SetIndent)
	var jsonBuf bytes.Buffer
	if err := WriteBackfillJSONL(&jsonBuf, decisions); err != nil {
		t.Fatalf("WriteBackfillJSONL: %v", err)
	}

	jsonLines := strings.Split(strings.TrimSpace(jsonBuf.String()), "\n")
	if len(jsonLines) != 3 {
		t.Fatalf("expected 3 JSON lines, got %d", len(jsonLines))
	}

	// Parse and verify each JSON line
	for i, jsonLine := range jsonLines {
		var parsed map[string]interface{}
		if err := json.Unmarshal([]byte(jsonLine), &parsed); err != nil {
			t.Fatalf("line %d: failed to parse JSON: %v", i, err)
		}

		if parsed["schedule"] != "daily-daily" {
			t.Errorf("line %d: expected schedule=daily-daily, got %v", i, parsed["schedule"])
		}

		expectedStatus := []string{"fresh", "stale", "error"}[i]
		if parsed["status"] != expectedStatus {
			t.Errorf("line %d: expected status=%s, got %v", i, expectedStatus, parsed["status"])
		}

		// Fresh case: backfill_from should not be present
		if i == 0 {
			if val, ok := parsed["backfill_from"]; ok && val != nil {
				t.Errorf("line %d: expected backfill_from absent or nil for fresh, got %v", i, val)
			}
		}
		// Stale case: should have stale_dates
		if i == 1 {
			staleDates, ok := parsed["stale_dates"].([]interface{})
			if !ok {
				t.Errorf("line %d: expected stale_dates to be an array, got %T", i, parsed["stale_dates"])
			} else if len(staleDates) != 3 {
				t.Errorf("line %d: expected 3 stale_dates, got %d", i, len(staleDates))
			}
		}
		// Error case: should have last_error
		if i == 2 {
			if parsed["last_error"] != "connection timeout" {
				t.Errorf("line %d: expected last_error=connection timeout, got %v", i, parsed["last_error"])
			}
		}
	}
}

func TestDecideBackfillNilObservation(t *testing.T) {
	// Empty observation with no status should not trigger error or backfill.
	obs := FreshnessObservation{}

	window := 7 * 24 * time.Hour
	dec := DecideBackfill(obs, window)

	// Should be fresh (empty = no missing, no gaps, no error).
	if dec.Status != "fresh" {
		t.Errorf("expected status=fresh for empty observation, got %s", dec.Status)
	}
}

func TestMakeRefreshStatus(t *testing.T) {
	now := time.Now().UTC()
	nextRun := now.Add(24 * time.Hour)

	// Case 1: Success status (obs is success, dec is fresh)
	obsSuccess := FreshnessObservation{
		Schedule:       "daily-success",
		Status:         "success",
		DuplicateCount: 2,
		ProviderDelay:  0,
	}
	decFresh := BackfillDecision{
		Status:       "fresh",
		MissingCount: 0,
		GapCount:     0,
	}
	status1 := MakeRefreshStatus(obsSuccess, decFresh, now, nextRun, 150)
	if status1.Status != "success" {
		t.Errorf("expected status=success, got %s", status1.Status)
	}
	if status1.ImportedBarCount != 150 {
		t.Errorf("expected imported_bar_count=150, got %d", status1.ImportedBarCount)
	}
	if !status1.LastSuccess.Equal(now) {
		t.Errorf("expected last_success=%v, got %v", now, status1.LastSuccess)
	}
	if !status1.NextRun.Equal(nextRun) {
		t.Errorf("expected next_run=%v, got %v", nextRun, status1.NextRun)
	}

	// Case 2: Stale status (obs is success, dec is stale)
	obsStale := FreshnessObservation{
		Schedule:       "daily-stale",
		Status:         "success",
		DuplicateCount: 0,
		ProviderDelay:  1,
	}
	decStale := BackfillDecision{
		Status:       "stale",
		MissingCount: 3,
		GapCount:     1,
	}
	status2 := MakeRefreshStatus(obsStale, decStale, now, nextRun, 0)
	if status2.Status != "stale" {
		t.Errorf("expected status=stale, got %s", status2.Status)
	}
	if status2.ImportedBarCount != 0 {
		t.Errorf("expected imported_bar_count=0, got %d", status2.ImportedBarCount)
	}

	// Case 3: Error status (obs is error or dec is error)
	obsErr := FreshnessObservation{
		Schedule:  "daily-err",
		Status:    "error",
		LastError: "connection timeout",
	}
	decErr := BackfillDecision{
		Status:    "error",
		LastError: "previous operation failed: connection timeout",
	}
	status3 := MakeRefreshStatus(obsErr, decErr, time.Time{}, nextRun, 0)
	if status3.Status != "error" {
		t.Errorf("expected status=error, got %s", status3.Status)
	}
	if status3.LastError != "connection timeout" {
		t.Errorf("expected last_error='connection timeout', got %q", status3.LastError)
	}
}

func TestWriteRefreshStatusTextAndJSONL(t *testing.T) {
	lastSuccess := time.Date(2026, 6, 20, 12, 0, 0, 0, time.UTC)
	nextRun := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)

	statuses := []RefreshStatus{
		{
			Schedule:         "daily-success",
			Status:           "success",
			LastSuccess:      lastSuccess,
			LastError:        "",
			NextRun:          nextRun,
			ImportedBarCount: 100,
			MissingCount:     0,
			GapCount:         0,
			DuplicateCount:   1,
			ProviderDelay:    0,
		},
		{
			Schedule:         "daily-error",
			Status:           "error",
			LastSuccess:      time.Time{}, // Zero time
			LastError:        "API key expired",
			NextRun:          time.Time{},
			ImportedBarCount: 0,
			MissingCount:     0,
			GapCount:         0,
			DuplicateCount:   0,
			ProviderDelay:    0,
		},
	}

	// Test Text output
	var textBuf bytes.Buffer
	if err := WriteRefreshStatusText(&textBuf, statuses); err != nil {
		t.Fatalf("WriteRefreshStatusText: %v", err)
	}

	text := textBuf.String()
	textLines := strings.Split(strings.TrimSpace(text), "\n")
	if len(textLines) != 2 {
		t.Errorf("expected 2 text lines, got %d", len(textLines))
	}

	// Check success line
	if !strings.Contains(textLines[0], "status=success") {
		t.Errorf("line 1 should contain status=success: %s", textLines[0])
	}
	if !strings.Contains(textLines[0], "last_success=2026-06-20T12:00:00Z") {
		t.Errorf("line 1 should contain last_success timestamp: %s", textLines[0])
	}
	if !strings.Contains(textLines[0], "imported_bar_count=100") {
		t.Errorf("line 1 should contain imported_bar_count=100: %s", textLines[0])
	}

	// Check error line
	if !strings.Contains(textLines[1], "status=error") {
		t.Errorf("line 2 should contain status=error: %s", textLines[1])
	}
	if !strings.Contains(textLines[1], "last_success=NONE") {
		t.Errorf("line 2 should contain last_success=NONE: %s", textLines[1])
	}
	if !strings.Contains(textLines[1], `last_error="API key expired"`) {
		t.Errorf("line 2 should contain last_error=\"API key expired\": %s", textLines[1])
	}

	// Test JSONL output
	var jsonBuf bytes.Buffer
	if err := WriteRefreshStatusJSONL(&jsonBuf, statuses); err != nil {
		t.Fatalf("WriteRefreshStatusJSONL: %v", err)
	}

	jsonLines := strings.Split(strings.TrimSpace(jsonBuf.String()), "\n")
	if len(jsonLines) != 2 {
		t.Fatalf("expected 2 JSON lines, got %d", len(jsonLines))
	}

	// Parse line 1
	var parsed1 map[string]interface{}
	if err := json.Unmarshal([]byte(jsonLines[0]), &parsed1); err != nil {
		t.Fatalf("failed to parse JSON line 1: %v", err)
	}
	if parsed1["schedule"] != "daily-success" {
		t.Errorf("expected schedule=daily-success, got %v", parsed1["schedule"])
	}
	if parsed1["status"] != "success" {
		t.Errorf("expected status=success, got %v", parsed1["status"])
	}
	if parsed1["last_success"] != "2026-06-20T12:00:00Z" {
		t.Errorf("expected last_success=2026-06-20T12:00:00Z, got %v", parsed1["last_success"])
	}
	if val, ok := parsed1["last_error"]; !ok || val != "" {
		t.Errorf("expected empty last_error to be present, got %v", val)
	}
	if val, ok := parsed1["imported_bar_count"]; !ok || val != 100.0 { // float64 in JSON unmarshal
		t.Errorf("expected imported_bar_count=100, got %v", val)
	}

	// Parse line 2
	var parsed2 map[string]interface{}
	if err := json.Unmarshal([]byte(jsonLines[1]), &parsed2); err != nil {
		t.Fatalf("failed to parse JSON line 2: %v", err)
	}
	if parsed2["status"] != "error" {
		t.Errorf("expected status=error, got %v", parsed2["status"])
	}
	if parsed2["last_success"] != nil {
		t.Errorf("expected last_success=nil, got %v", parsed2["last_success"])
	}
	if parsed2["last_error"] != "API key expired" {
		t.Errorf("expected last_error=API key expired, got %v", parsed2["last_error"])
	}
	if val, ok := parsed2["imported_bar_count"]; !ok || val != 0.0 {
		t.Errorf("expected zero imported_bar_count to be present, got %v", val)
	}
}
