package main

import (
	"context"
	"os"
	"path/filepath"
	"sync"
	"testing"
	"time"

	"git.toki-labs.com/toki/alt/services/worker/internal/config"
	"git.toki-labs.com/toki/alt/services/worker/internal/jobs"
	"git.toki-labs.com/toki/alt/services/worker/internal/providers/kis"
	"git.toki-labs.com/toki/alt/services/worker/internal/scheduler"
	"git.toki-labs.com/toki/alt/services/worker/internal/storage/postgres"
)

// TestStoreBackedDepsWiresBacktestSurfaces guards against a regression where the
// worker process serves backtest reads but leaves the start surface (Starter)
// unwired, which would advertise no backtest-start capability and reject every
// start command. The store is built with a nil pool because the wiring path only
// constructs adapters; it never touches the database at startup.
func TestStoreBackedDepsWiresBacktestSurfaces(t *testing.T) {
	runner := jobs.NewRunner()
	deps := storeBackedDeps(runner, postgres.NewStore(nil))

	if deps.Starter == nil {
		t.Error("expected backtest Starter to be wired")
	}
	if deps.Analysis == nil || deps.Results == nil {
		t.Error("expected backtest read stores to be wired")
	}
	if deps.Instruments == nil || deps.Bars == nil {
		t.Error("expected market stores to be wired")
	}
	if deps.DailyBarImporter == nil {
		t.Error("expected daily bar importer to be wired")
	}
	if deps.Paper == nil {
		t.Error("expected paper trading service to be wired")
	}
	if deps.Live == nil {
		t.Error("expected live trading service to be wired (audit query surface must be up even without a broker)")
	}

	// Wiring registers the import and run-backtest job handlers on the runner, so
	// it must hold at least those two handlers after wiring.
	if runner.Len() < 2 {
		t.Errorf("expected wiring to register job handlers, runner holds %d", runner.Len())
	}
}

func TestStartScheduler(t *testing.T) {
	statusStore := scheduler.NewRefreshStatusStore()

	// 1. Scheduler disabled (no config path)
	cfg := config.Config{
		SchedulerEnabled: false,
	}
	runner := jobs.NewRunner()
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := startScheduler(ctx, cfg, runner, statusStore)
	if err != nil {
		t.Fatalf("expected nil error for disabled scheduler, got %v", err)
	}

	// 2. Scheduler enabled with invalid config path (should fail)
	cfg.SchedulerEnabled = true
	cfg.ScheduleConfigPath = "non_existent_file.yaml"
	err = startScheduler(ctx, cfg, runner, statusStore)
	if err == nil {
		t.Fatal("expected error for non existent schedule config, got nil")
	}

	// 3. Scheduler enabled with valid config
	tmpDir := t.TempDir()
	tmpFile := filepath.Join(tmpDir, "schedule.yaml")
	if err := os.WriteFile(tmpFile, []byte(`
schedules:
  - name: kr-core-daily
    provider: kis
    selector:
      kind: watchlist
      market: KR
      venue: KRX
      name: kr-core
      symbols: ["005930"]
    timeframe: daily
    cadence: daily
    timezone: Asia/Seoul
    backfill_window: 3d
    parallelism_limit: 2
`), 0644); err != nil {
		t.Fatalf("failed to write temp file: %v", err)
	}

	cfg.ScheduleConfigPath = tmpFile
	err = startScheduler(ctx, cfg, runner, statusStore)
	if err != nil {
		t.Fatalf("expected no error for valid config, got %v", err)
	}
}

type mockExecutor struct {
	mu         sync.Mutex
	dispatched int
}

func (m *mockExecutor) Execute(ctx context.Context, job jobs.Job) error {
	m.mu.Lock()
	m.dispatched++
	m.mu.Unlock()
	return nil
}

func (m *mockExecutor) ExecuteWithResult(ctx context.Context, job jobs.Job) (jobs.Result, bool, error) {
	return jobs.Result{}, false, nil
}

// mockExecutorSuccess always succeeds but returns NO import evidence.
type mockExecutorSuccess struct{}

func (m *mockExecutorSuccess) Execute(ctx context.Context, job jobs.Job) error {
	return nil
}

func (m *mockExecutorSuccess) ExecuteWithResult(ctx context.Context, job jobs.Job) (jobs.Result, bool, error) {
	// No evidence: success but no import bars.
	return jobs.Result{}, false, nil
}

// mockExecutorWithEvidence always succeeds and returns actual import evidence.
type mockExecutorWithEvidence struct {
	bars        int
	instruments int
}

func (m *mockExecutorWithEvidence) Execute(ctx context.Context, job jobs.Job) error {
	return nil
}

func (m *mockExecutorWithEvidence) ExecuteWithResult(ctx context.Context, job jobs.Job) (jobs.Result, bool, error) {
	return jobs.Result{
		Bars:        m.bars,
		Instruments: m.instruments,
	}, true, nil
}

func TestSchedulerLoopDispatchesTickAndStops(t *testing.T) {
	schedCfg := scheduler.Config{
		Schedules: []scheduler.ScheduleConfig{
			{
				Name:     "kr-core-daily-mock",
				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,
			},
		},
	}

	exec := &mockExecutor{}
	opts := scheduler.TickOptions{
		Capabilities: scheduler.CapabilityMap(kis.Capability()),
		Executor:     exec,
	}

	ctx, cancel := context.WithCancel(context.Background())
	ticks := make(chan time.Time)

	loopDone := make(chan struct{})
	statusStore := scheduler.NewRefreshStatusStore()
	go func() {
		runSchedulerLoop(ctx, schedCfg, opts, ticks, statusStore)
		close(loopDone)
	}()

	// Send 1 tick
	ticks <- time.Now()

	// Wait briefly for dispatch
	time.Sleep(10 * time.Millisecond)

	exec.mu.Lock()
	dispatchedVal := exec.dispatched
	exec.mu.Unlock()

	if dispatchedVal == 0 {
		t.Error("expected at least 1 job dispatch on tick")
	}

	// Cancel context to shutdown
	cancel()

	select {
	case <-loopDone:
		// success: loop stopped
	case <-time.After(1 * time.Second):
		t.Error("scheduler loop did not stop on context cancellation")
	}
}

// TestSchedulerLoopRecordsTickAndStatusStore verifies that a no-evidence success
// is classified as "stale" (not "success") and does NOT update last_success.
func TestSchedulerLoopRecordsTickAndStatusStore(t *testing.T) {
	schedCfg := scheduler.Config{
		Schedules: []scheduler.ScheduleConfig{
			{
				Name:     "kr-core-daily-mock",
				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,
			},
		},
	}

	// mockExecutorSuccess always succeeds but returns NO import evidence.
	execSucceed := &mockExecutorSuccess{}
	opts := scheduler.TickOptions{
		Capabilities: scheduler.CapabilityMap(kis.Capability()),
		Executor:     execSucceed,
	}

	ctx, cancel := context.WithCancel(context.Background())
	ticks := make(chan time.Time)

	loopDone := make(chan struct{})
	statusStore := scheduler.NewRefreshStatusStore()
	go func() {
		runSchedulerLoop(ctx, schedCfg, opts, ticks, statusStore)
		close(loopDone)
	}()

	// Send a tick and wait for processing.
	now := time.Now()
	ticks <- now
	time.Sleep(50 * time.Millisecond)

	// Verify that the status store recorded a "stale" entry (no evidence).
	entry, ok := statusStore.Get("kr-core-daily-mock")
	if !ok {
		t.Fatal("expected status store entry for schedule after tick")
	}
	if entry.Status != "stale" {
		t.Errorf("status = %q, want %q (no evidence → stale)", entry.Status, "stale")
	}
	// CRITICAL: without ImportSummary from executor, ImportedBarCount MUST be 0.
	if entry.ImportedBarCount != 0 {
		t.Errorf("imported_bar_count = %d, want 0 (no evidence from mock executor)", entry.ImportedBarCount)
	}
	// last_success must NOT be set for no-evidence.
	if !entry.LastSuccess.IsZero() {
		t.Errorf("last_success = %v, want zero (no evidence)", entry.LastSuccess)
	}

	cancel()
	select {
	case <-loopDone:
	case <-time.After(1 * time.Second):
		t.Error("scheduler loop did not stop on context cancellation")
	}
}

// TestSchedulerLoopWithActualEvidence proves that when the executor returns
// actual RefreshEvidence, the status store correctly propagates imported bar counts.
func TestSchedulerLoopWithActualEvidence(t *testing.T) {
	schedCfg := scheduler.Config{
		Schedules: []scheduler.ScheduleConfig{
			{
				Name:     "kr-core-daily-evidence",
				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,
			},
		},
	}

	// mockExecutorWithEvidence returns actual import evidence.
	execWithEvid := &mockExecutorWithEvidence{bars: 120, instruments: 1}
	opts := scheduler.TickOptions{
		Capabilities: scheduler.CapabilityMap(kis.Capability()),
		Executor:     execWithEvid,
	}

	ctx, cancel := context.WithCancel(context.Background())
	ticks := make(chan time.Time)

	loopDone := make(chan struct{})
	statusStore := scheduler.NewRefreshStatusStore()
	go func() {
		runSchedulerLoop(ctx, schedCfg, opts, ticks, statusStore)
		close(loopDone)
	}()

	now := time.Now()
	ticks <- now
	time.Sleep(50 * time.Millisecond)

	// Verify actual evidence propagation.
	entry, ok := statusStore.Get("kr-core-daily-evidence")
	if !ok {
		t.Fatal("expected status store entry after tick with evidence")
	}
	if entry.Status != "success" {
		t.Errorf("status = %q, want %q", entry.Status, "success")
	}
	if entry.ImportedBarCount != 120 {
		t.Errorf("imported_bar_count = %d, want 120", entry.ImportedBarCount)
	}
	if entry.LastSuccess.IsZero() {
		t.Error("last_success should be set when evidence is present")
	}

	cancel()
	select {
	case <-loopDone:
	case <-time.After(1 * time.Second):
		t.Error("scheduler loop did not stop on context cancellation")
	}
}

func TestRecordTickSkippedItemsNotSuccess(t *testing.T) {
	store := scheduler.NewRefreshStatusStore()
	now := time.Now()

	// All items are skipped (e.g., duplicate window running).
	items := []scheduler.ItemResult{
		{Schedule: "test", Symbol: "005930", Status: "skipped", Provider: "kis", Timeframe: "daily", Readiness: "stale", Freshness: "stale", Error: "duplicate window running"},
	}
	store.RecordTick("test", items, now)

	entry, ok := store.Get("test")
	if !ok {
		t.Fatal("expected entry after recording skipped items")
	}
	if entry.Status != "stale" {
		t.Errorf("status = %q, want %q", entry.Status, "stale")
	}
}

func TestRecordTickFailedItemsError(t *testing.T) {
	store := scheduler.NewRefreshStatusStore()
	now := time.Now()

	items := []scheduler.ItemResult{
		{Schedule: "test", Symbol: "005930", Status: "failed", Provider: "kis", Timeframe: "daily", Readiness: "error", Freshness: "stale", Error: "connection timeout"},
	}
	store.RecordTick("test", items, now)

	entry, ok := store.Get("test")
	if !ok {
		t.Fatal("expected entry after recording failed items")
	}
	if entry.Status != "error" {
		t.Errorf("status = %q, want %q", entry.Status, "error")
	}
	if entry.LastError != "connection timeout" {
		t.Errorf("last_error = %q, want %q", entry.LastError, "connection timeout")
	}
}

func TestRecordTickMixedSucceededSkippedStale(t *testing.T) {
	store := scheduler.NewRefreshStatusStore()
	now := time.Now()

	// Some succeeded, some skipped: should be "stale" not "success".
	items := []scheduler.ItemResult{
		{Schedule: "test", Symbol: "005930", Status: "succeeded", Provider: "kis", Timeframe: "daily"},
		{Schedule: "test", Symbol: "000660", Status: "skipped", Provider: "kis", Timeframe: "daily", Error: "duplicate window running"},
	}
	store.RecordTick("test", items, now)

	entry, ok := store.Get("test")
	if !ok {
		t.Fatal("expected entry after recording mixed items")
	}
	if entry.Status != "stale" {
		t.Errorf("status = %q, want %q", entry.Status, "stale")
	}
	// imported_bar_count should be 0 for stale.
	if entry.ImportedBarCount != 0 {
		t.Errorf("imported_bar_count = %d, want 0 for stale", entry.ImportedBarCount)
	}
}

func TestRecordTickPreservesLastSuccessOnStale(t *testing.T) {
	store := scheduler.NewRefreshStatusStore()
	now := time.Now()

	// First: evidence-backed success.
	store.RecordTick("test", []scheduler.ItemResult{
		{
			Schedule:  "test",
			Symbol:    "005930",
			Status:    "succeeded",
			Provider:  "kis",
			Timeframe: "daily",
			ImportSummary: &scheduler.RefreshEvidence{
				HasBars:      true,
				ImportedBars: 100,
			},
		},
	}, now)

	prev, _ := store.Get("test")
	if prev.Status != "success" {
		t.Fatalf("first tick status = %q, want %q", prev.Status, "success")
	}
	if prev.LastSuccess.IsZero() {
		t.Fatal("last_success should be set after evidence-backed tick")
	}

	// Second: all skipped (stale). Should preserve last_success.
	store.RecordTick("test", []scheduler.ItemResult{
		{Schedule: "test", Symbol: "005930", Status: "skipped", Provider: "kis", Timeframe: "daily", Error: "duplicate"},
	}, now.Add(1*time.Minute))

	stale, _ := store.Get("test")
	if stale.Status != "stale" {
		t.Errorf("stale status = %q, want %q", stale.Status, "stale")
	}
	if !stale.LastSuccess.Equal(prev.LastSuccess) {
		t.Errorf("last_success was updated on stale tick: was %v, still %v", prev.LastSuccess, stale.LastSuccess)
	}
}

func TestRecordTickUnknownStatusNoSuccessEvidence(t *testing.T) {
	store := scheduler.NewRefreshStatusStore()
	now := time.Now()

	// Unknown status should not create success evidence.
	store.RecordTick("test", []scheduler.ItemResult{
		{Schedule: "test", Symbol: "005930", Status: "unknown", Provider: "kis", Timeframe: "daily"},
	}, now)

	entry, ok := store.Get("test")
	if !ok {
		t.Fatal("expected entry after recording unknown status")
	}
	if entry.Status == "success" {
		t.Errorf("status = %q for unknown item, should NOT be %q", entry.Status, "success")
	}
}

func TestRecordTickZeroItemsSkipped(t *testing.T) {
	store := scheduler.NewRefreshStatusStore()
	now := time.Now()

	// Zero items: should not create any entry.
	store.RecordTick("test", []scheduler.ItemResult{}, now)

	_, ok := store.Get("test")
	if ok {
		t.Error("expected no entry when zero items recorded")
	}
}

func TestSchedulerLoopConfigWiring(t *testing.T) {
	// Verify that startScheduler saves the schedule config to statusStore
	// so that next_run calculations work correctly.
	statusStore := scheduler.NewRefreshStatusStore()

	tmpDir := t.TempDir()
	tmpFile := filepath.Join(tmpDir, "schedule.yaml")
	if err := os.WriteFile(tmpFile, []byte(`
schedules:
  - name: kr-core-daily-wire
    provider: kis
    selector:
      kind: watchlist
      market: KR
      venue: KRX
      name: kr-core
      symbols: ["005930"]
    timeframe: daily
    cadence: daily
    timezone: Asia/Seoul
    backfill_window: 3d
    parallelism_limit: 2
`), 0644); err != nil {
		t.Fatalf("failed to write temp file: %v", err)
	}

	cfg := config.Config{
		SchedulerEnabled:   true,
		ScheduleConfigPath: tmpFile,
	}
	runner := jobs.NewRunner()
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := startScheduler(ctx, cfg, runner, statusStore)
	if err != nil {
		t.Fatalf("startScheduler: %v", err)
	}

	// Verify that the schedule config was stored.
	storedCfg := statusStore.ScheduleConfig()
	if len(storedCfg.Schedules) != 1 {
		t.Errorf("stored config has %d schedules, want 1", len(storedCfg.Schedules))
	}
	if len(storedCfg.Schedules) > 0 && storedCfg.Schedules[0].Name != "kr-core-daily-wire" {
		t.Errorf("schedule name = %q, want %q", storedCfg.Schedules[0].Name, "kr-core-daily-wire")
	}
}

// TestSchedulerLoopMixedEvidenceNoEvidenceSuccessPreservesLastSuccess proves that
// a no-evidence tick does NOT overwrite last_success from a previous evidence-backed
// success. After this change, no-evidence succeeds are classified as "stale" (not
// "success"), so the previous evidence-backed "success" state is preserved.
func TestSchedulerLoopMixedEvidenceNoEvidenceSuccessPreservesLastSuccess(t *testing.T) {
	schedCfg := scheduler.Config{
		Schedules: []scheduler.ScheduleConfig{
			{
				Name:     "kr-core-daily-mixed",
				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,
			},
		},
	}

	execWithEvid := &mockExecutorWithEvidence{bars: 100, instruments: 1}
	opts := scheduler.TickOptions{
		Capabilities: scheduler.CapabilityMap(kis.Capability()),
		Executor:     execWithEvid,
	}

	ctx, cancel := context.WithCancel(context.Background())
	ticks := make(chan time.Time)
	loopDone := make(chan struct{})
	statusStore := scheduler.NewRefreshStatusStore()
	go func() {
		runSchedulerLoop(ctx, schedCfg, opts, ticks, statusStore)
		close(loopDone)
	}()

	now := time.Now()
	ticks <- now
	time.Sleep(50 * time.Millisecond)

	// Verify evidence-backed success.
	entry1, ok := statusStore.Get("kr-core-daily-mixed")
	if !ok {
		t.Fatal("expected entry after evidence-backed tick")
	}
	if entry1.Status != "success" {
		t.Errorf("status = %q, want %q", entry1.Status, "success")
	}
	if entry1.ImportedBarCount != 100 {
		t.Errorf("imported_bar_count = %d, want 100", entry1.ImportedBarCount)
	}
	if entry1.LastSuccess.IsZero() {
		t.Fatal("last_success should be set after evidence-backed tick")
	}
	lastSuccessAfterEvidence := entry1.LastSuccess

	cancel()
	select {
	case <-loopDone:
	case <-time.After(1 * time.Second):
		t.Error("scheduler loop did not stop on context cancellation")
	}

	// Now test no-evidence preservation via direct RecordTick calls.
	// This validates that calling RecordTick with no-evidence (ImportSummary == nil)
	// does NOT overwrite the previous evidence-backed "success" state.
	entry2, _ := statusStore.Get("kr-core-daily-mixed")
	if entry2.Status != "success" {
		t.Errorf("pre-test status = %q, want %q", entry2.Status, "success")
	}

	// RecordTick with no-evidence success item.
	statusStore.RecordTick("kr-core-daily-mixed", []scheduler.ItemResult{
		{Schedule: "kr-core-daily-mixed", Symbol: "005930", Status: "succeeded", Provider: "kis", Timeframe: "daily", Readiness: "ready", Freshness: "fresh"},
	}, now.Add(1*time.Minute))

	entry3, ok := statusStore.Get("kr-core-daily-mixed")
	if !ok {
		t.Fatal("expected entry after RecordTick")
	}
	// No-evidence tick replaces previous "success" → the entry becomes "stale"
	// because all items have no evidence. The previous last_success must NOT
	// be overwritten, but the status itself is now "stale".
	if entry3.Status != "stale" {
		t.Errorf("post-no-evidence status = %q, want %q (no evidence → stale)", entry3.Status, "stale")
	}
	// last_success must NOT be updated by no-evidence tick.
	if !entry3.LastSuccess.Equal(lastSuccessAfterEvidence) {
		t.Errorf("last_success = %v, want %v (preserved from evidence tick)", entry3.LastSuccess, lastSuccessAfterEvidence)
	}
	// imported_bar_count stays the same.
	if entry3.ImportedBarCount != 100 {
		t.Errorf("imported_bar_count = %d, want 100 (no evidence to add)", entry3.ImportedBarCount)
	}
}

// TestDirectRecordTickNoEvidenceSuccessNoLastSuccess verifies that no-evidence
// success is classified as "stale" and does NOT set last_success.
func TestDirectRecordTickNoEvidenceSuccessNoLastSuccess(t *testing.T) {
	store := scheduler.NewRefreshStatusStore()
	now := time.Now()

	// Direct RecordTick with no-evidence success (ImportSummary == nil).
	items := []scheduler.ItemResult{
		{Schedule: "test", Symbol: "005930", Status: "succeeded", Provider: "kis", Timeframe: "daily", Readiness: "ready", Freshness: "fresh"},
	}
	store.RecordTick("test", items, now)

	entry, ok := store.Get("test")
	if !ok {
		t.Fatal("expected entry")
	}
	if entry.Status != "stale" {
		t.Errorf("status = %q, want %q (no evidence → stale)", entry.Status, "stale")
	}
	// No evidence: status is "stale", so last_success must NOT be set.
	if !entry.LastSuccess.IsZero() {
		t.Errorf("last_success = %v, want zero (no evidence)", entry.LastSuccess)
	}
	// No evidence: ImportedBarCount should be 0.
	if entry.ImportedBarCount != 0 {
		t.Errorf("imported_bar_count = %d, want 0", entry.ImportedBarCount)
	}
}

// TestDirectRecordTickEvidenceSuccess sets last_success.
func TestDirectRecordTickEvidenceSuccessSetsLastSuccess(t *testing.T) {
	store := scheduler.NewRefreshStatusStore()
	now := time.Now()

	items := []scheduler.ItemResult{
		{
			Schedule:  "test",
			Symbol:    "005930",
			Status:    "succeeded",
			Provider:  "kis",
			Timeframe: "daily",
			Readiness: "ready",
			Freshness: "fresh",
			ImportSummary: &scheduler.RefreshEvidence{
				HasBars:      true,
				ImportedBars: 150,
			},
		},
	}
	store.RecordTick("test", items, now)

	entry, ok := store.Get("test")
	if !ok {
		t.Fatal("expected entry")
	}
	if entry.Status != "success" {
		t.Errorf("status = %q, want %q", entry.Status, "success")
	}
	if entry.ImportedBarCount != 150 {
		t.Errorf("imported_bar_count = %d, want 150", entry.ImportedBarCount)
	}
	if entry.LastSuccess.IsZero() {
		t.Error("last_success should be set when evidence is present")
	}
}

// TestDirectRecordTickMixedEvidenceDoesNotUpgradeSuccess proves that a mixed
// tick (some succeeded with evidence, some succeeded without) does NOT silently
// upgrade to full success evidence. Only items with evidence contribute bar
// counts; the status is still "success" because all items succeeded, but
// last_success is only updated when hasEvidence is true.
func TestDirectRecordTickMixedEvidenceDoesNotUpgradeSuccess(t *testing.T) {
	store := scheduler.NewRefreshStatusStore()
	now := time.Now()

	// Two items: one with evidence, one without.
	items := []scheduler.ItemResult{
		{
			Schedule:  "test",
			Symbol:    "005930",
			Status:    "succeeded",
			Provider:  "kis",
			Timeframe: "daily",
			ImportSummary: &scheduler.RefreshEvidence{
				HasBars:      true,
				ImportedBars: 100,
			},
		},
		{
			Schedule:  "test",
			Symbol:    "000660",
			Status:    "succeeded",
			Provider:  "kis",
			Timeframe: "daily",
			// No ImportSummary — no evidence.
		},
	}
	store.RecordTick("test", items, now)

	entry, ok := store.Get("test")
	if !ok {
		t.Fatal("expected entry")
	}
	// All items succeeded → status is "success" even with mixed evidence.
	if entry.Status != "success" {
		t.Errorf("status = %q, want %q", entry.Status, "success")
	}
	// Only the item with evidence contributes bar counts.
	if entry.ImportedBarCount != 100 {
		t.Errorf("imported_bar_count = %d, want 100", entry.ImportedBarCount)
	}
	// hasEvidence is true (at least one item has evidence) → last_success is set.
	if entry.LastSuccess.IsZero() {
		t.Error("last_success should be set when hasEvidence is true (mixed with at least one evidence)")
	}
}
