package socket

import (
	"testing"
	"time"

	altv1 "git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1"
	"git.toki-labs.com/toki/alt/services/worker/internal/scheduler"
)

// TestSchedulerHandlersRegisterWhenRefreshStatusIsSet verifies that the
// SchedulerRefreshStatusRequest handler is registered only when the
// RefreshStatus dependency is wired, and that the handler returns all entries
// (success, stale, error) with the expected protobuf fields.
func TestSchedulerHandlersRegisterWhenRefreshStatusIsSet(t *testing.T) {
	store := scheduler.NewRefreshStatusStore()
	store.SetScheduleConfig(scheduler.Config{
		Schedules: []scheduler.ScheduleConfig{
			{
				Name:     "kr-daily",
				Provider: "kis",
				Selector: scheduler.SelectorConfig{
					Kind:    "watchlist",
					Market:  "KR",
					Venue:   "KRX",
					Name:    "kr-core",
					Symbols: []string{"005930"},
				},
				Timeframe:        "daily",
				Cadence:          "daily",
				Timezone:         "Asia/Seoul",
				BackfillWindow:   "3d",
				ParallelismLimit: 2,
			},
		},
	})
	store.Record("kr-daily", scheduler.RefreshStatus{
		Schedule:         "kr-daily",
		Status:           "success",
		ImportedBarCount: 120,
		MissingCount:     0,
	})

	installed := marketHandlers(Deps{RefreshStatus: store})

	// Verify the SchedulerRefreshStatusRequest handler is present.
	found := false
	for _, h := range installed {
		if h.requestType == "alt.v1.SchedulerRefreshStatusRequest" {
			found = true
			if h.register == nil {
				t.Error("SchedulerRefreshStatusRequest handler has nil register")
			}
			break
		}
	}
	if !found {
		t.Error("marketHandlers should include SchedulerRefreshStatusRequest handler when RefreshStatus is set")
	}

	// Verify All() returns the entry we recorded.
	all := store.All()
	if len(all) != 1 {
		t.Fatalf("expected 1 entry from store.All(), got %d", len(all))
	}
	if all[0].Schedule != "kr-daily" || all[0].Status != "success" {
		t.Errorf("unexpected entry: %+v", all[0])
	}

	// Verify Get() by name works.
	entry, ok := store.Get("kr-daily")
	if !ok {
		t.Error("store.Get('kr-daily') should return ok=true")
	}
	if entry.Schedule != "kr-daily" {
		t.Errorf("expected schedule 'kr-daily', got %q", entry.Schedule)
	}
}

func TestSchedulerHandlersOmittedWhenRefreshStatusIsNil(t *testing.T) {
	installed := marketHandlers(Deps{})
	for _, h := range installed {
		if h.requestType == "alt.v1.SchedulerRefreshStatusRequest" {
			t.Error("marketHandlers should NOT include SchedulerRefreshStatusRequest handler when RefreshStatus is nil")
		}
	}
}

func TestSchedulerRefreshStatusResponseFields(t *testing.T) {
	store := scheduler.NewRefreshStatusStore()
	store.SetScheduleConfig(scheduler.Config{
		Schedules: []scheduler.ScheduleConfig{
			{
				Name:             "kr-daily",
				Provider:         "kis",
				BackfillWindow:   "3d",
				Cadence:          "daily",
				Timeframe:        "daily",
				Timezone:         "Asia/Seoul",
				ParallelismLimit: 2,
				Selector: scheduler.SelectorConfig{
					Kind:    "watchlist",
					Market:  "KR",
					Venue:   "KRX",
					Name:    "kr-core",
					Symbols: []string{"005930"},
				},
			},
		},
	})
	store.Record("kr-daily", scheduler.RefreshStatus{
		Schedule:         "kr-daily",
		Status:           "success",
		LastSuccess:      time.UnixMilli(1700000000000),
		ImportedBarCount: 120,
		MissingCount:     0,
		GapCount:         0,
		DuplicateCount:   0,
		ProviderDelay:    1,
	})

	deps := Deps{RefreshStatus: store}
	resp, err := handleSchedulerRefreshStatus(deps, &altv1.SchedulerRefreshStatusRequest{})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(resp.GetEntries()) != 1 {
		t.Fatalf("expected 1 entry, got %d", len(resp.GetEntries()))
	}
	entry := resp.GetEntries()[0]
	if entry.GetSchedule() != "kr-daily" {
		t.Errorf("schedule = %q, want %q", entry.GetSchedule(), "kr-daily")
	}
	if entry.GetStatus() != "success" {
		t.Errorf("status = %q, want %q", entry.GetStatus(), "success")
	}
	if entry.GetImportedBarCount() != 120 {
		t.Errorf("imported_bar_count = %d, want 120", entry.GetImportedBarCount())
	}
	if entry.GetMissingCount() != 0 {
		t.Errorf("missing_count = %d, want 0", entry.GetMissingCount())
	}
	if entry.GetGapCount() != 0 {
		t.Errorf("gap_count = %d, want 0", entry.GetGapCount())
	}
	if entry.GetDuplicateCount() != 0 {
		t.Errorf("duplicate_count = %d, want 0", entry.GetDuplicateCount())
	}
	if entry.GetProviderDelayDays() != 1 {
		t.Errorf("provider_delay_days = %d, want 1", entry.GetProviderDelayDays())
	}
}

func TestSchedulerRefreshStatusZeroTimestamps(t *testing.T) {
	// Test 1: Zero LastSuccess and zero NextRun with NO config.
	// When config is absent, enrichWithNextRun does not compute NextRun,
	// so both timestamps remain zero and unixMilliMs converts them to 0.
	store := scheduler.NewRefreshStatusStore()
	// Do NOT set schedule config — NextRun stays zero.
	store.Record("kr-daily", scheduler.RefreshStatus{
		Schedule:         "kr-daily",
		Status:           "stale",
		LastSuccess:      time.Time{}, // zero time
		NextRun:          time.Time{}, // zero time (no config to enrich)
		ImportedBarCount: 0,
		MissingCount:     0,
		GapCount:         0,
		DuplicateCount:   0,
		ProviderDelay:    0,
	})

	resp, err := handleSchedulerRefreshStatus(Deps{RefreshStatus: store}, &altv1.SchedulerRefreshStatusRequest{})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(resp.GetEntries()) != 1 {
		t.Fatalf("expected 1 entry, got %d", len(resp.GetEntries()))
	}
	entry := resp.GetEntries()[0]
	if entry.GetLastSuccessUnixMs() != 0 {
		t.Errorf("LastSuccessUnixMs = %d, want 0 for zero time", entry.GetLastSuccessUnixMs())
	}
	if entry.GetNextRunUnixMs() != 0 {
		t.Errorf("NextRunUnixMs = %d, want 0 for zero time (no config)", entry.GetNextRunUnixMs())
	}

	// Test 2: Non-zero LastSuccess with config.
	// When config is set, NextRun gets enriched by enrichWithNextRun,
	// so only LastSuccess is tested for non-zero preservation.
	store2 := scheduler.NewRefreshStatusStore()
	store2.SetScheduleConfig(scheduler.Config{
		Schedules: []scheduler.ScheduleConfig{
			{
				Name:             "kr-daily",
				Provider:         "kis",
				BackfillWindow:   "3d",
				Cadence:          "daily",
				Timeframe:        "daily",
				Timezone:         "Asia/Seoul",
				ParallelismLimit: 2,
				Selector: scheduler.SelectorConfig{
					Kind:    "watchlist",
					Market:  "KR",
					Venue:   "KRX",
					Name:    "kr-core",
					Symbols: []string{"005930"},
				},
			},
		},
	})
	nonZeroSuccess := time.UnixMilli(1700000000000)
	store2.Record("kr-daily", scheduler.RefreshStatus{
		Schedule:         "kr-daily",
		Status:           "success",
		LastSuccess:      nonZeroSuccess,
		NextRun:          time.Time{}, // will be enriched by enrichWithNextRun
		ImportedBarCount: 120,
		MissingCount:     0,
		GapCount:         0,
		DuplicateCount:   0,
		ProviderDelay:    1,
	})

	resp2, err := handleSchedulerRefreshStatus(Deps{RefreshStatus: store2}, &altv1.SchedulerRefreshStatusRequest{})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(resp2.GetEntries()) != 1 {
		t.Fatalf("expected 1 entry, got %d", len(resp2.GetEntries()))
	}
	entry2 := resp2.GetEntries()[0]
	if entry2.GetLastSuccessUnixMs() != 1700000000000 {
		t.Errorf("LastSuccessUnixMs = %d, want 1700000000000", entry2.GetLastSuccessUnixMs())
	}
	// NextRun is enriched by enrichWithNextRun — must NOT be zero.
	if entry2.GetNextRunUnixMs() == 0 {
		t.Errorf("NextRunUnixMs = 0, expected enriched non-zero value")
	}
	if entry2.GetImportedBarCount() != 120 {
		t.Errorf("imported_bar_count = %d, want 120", entry2.GetImportedBarCount())
	}
	if entry2.GetProviderDelayDays() != 1 {
		t.Errorf("provider_delay_days = %d, want 1", entry2.GetProviderDelayDays())
	}

	// Test 3: Error status with zero timestamps and NO config.
	store3 := scheduler.NewRefreshStatusStore()
	// Do NOT set schedule config.
	store3.Record("kr-daily", scheduler.RefreshStatus{
		Schedule:         "kr-daily",
		Status:           "error",
		LastSuccess:      time.Time{}, // zero - never succeeded
		LastError:        "API key expired",
		NextRun:          time.Time{}, // zero
		ImportedBarCount: 0,
		MissingCount:     0,
		GapCount:         0,
		DuplicateCount:   0,
		ProviderDelay:    0,
	})

	resp3, err := handleSchedulerRefreshStatus(Deps{RefreshStatus: store3}, &altv1.SchedulerRefreshStatusRequest{})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(resp3.GetEntries()) != 1 {
		t.Fatalf("expected 1 entry, got %d", len(resp3.GetEntries()))
	}
	entry3 := resp3.GetEntries()[0]
	if entry3.GetStatus() != "error" {
		t.Errorf("status = %q, want %q", entry3.GetStatus(), "error")
	}
	if entry3.GetLastSuccessUnixMs() != 0 {
		t.Errorf("LastSuccessUnixMs = %d, want 0 for zero time on error status", entry3.GetLastSuccessUnixMs())
	}
	if entry3.GetNextRunUnixMs() != 0 {
		t.Errorf("NextRunUnixMs = %d, want 0 for zero time on error status (no config)", entry3.GetNextRunUnixMs())
	}
	if entry3.GetLastError() != "API key expired" {
		t.Errorf("last_error = %q, want %q", entry3.GetLastError(), "API key expired")
	}
}

func TestSchedulerCapabilitiesAddedWhenRefreshStatusIsWired(t *testing.T) {
	capsNo := capabilitiesForDeps(Deps{})
	capsYes := capabilitiesForDeps(Deps{
		RefreshStatus: scheduler.NewRefreshStatusStore(),
	})

	foundNo := false
	for _, c := range capsNo {
		if c == "scheduler" {
			foundNo = true
		}
	}
	if foundNo {
		t.Error("capabilities should NOT include 'scheduler' when RefreshStatus is nil")
	}

	foundYes := false
	for _, c := range capsYes {
		if c == "scheduler" {
			foundYes = true
			break
		}
	}
	if !foundYes {
		t.Error("capabilities should include 'scheduler' when RefreshStatus is wired")
	}
}
