package jobs

import (
	"context"
	"encoding/json"
	"testing"
	"time"

	"git.toki-labs.com/toki/alt/packages/domain/market"
	"git.toki-labs.com/toki/alt/services/worker/internal/marketdata/importer"
)

// stubImporter records the request it was dispatched with.
type stubImporter struct {
	called  bool
	request importer.DailyBarRequest
	result  importer.Result
}

func (s *stubImporter) ImportDailyBars(_ context.Context, request importer.DailyBarRequest) (importer.Result, error) {
	s.called = true
	s.request = request
	return s.result, nil
}

// kisCapability mirrors the KIS provider capability the live worker wires in:
// KR/KRX plus the US venues, daily bars only.
func kisCapability() market.ProviderCapability {
	return market.ProviderCapability{
		Provider: market.ProviderKIS,
		Venues: map[market.Venue]bool{
			market.VenueKRX:    true,
			market.VenueNASDAQ: true,
			market.VenueNYSE:   true,
		},
		Timeframes: map[market.Timeframe]bool{market.TimeframeDaily: true},
	}
}

// krOnlyCapability is a stand-in for a future KR-only provider; it must reject
// US venue imports without any KR-specific code change.
func krOnlyCapability() market.ProviderCapability {
	return market.ProviderCapability{
		Provider:   market.Provider("kiwoom"),
		Venues:     map[market.Venue]bool{market.VenueKRX: true},
		Timeframes: map[market.Timeframe]bool{market.TimeframeDaily: true},
	}
}

func TestRegisterDailyBarImportHandlerDispatchesImporter(t *testing.T) {
	runner := NewRunner()
	imp := &stubImporter{result: importer.Result{Instruments: 1, Bars: 2}}
	RegisterDailyBarImportHandler(runner, kisCapability(), imp)

	payload := json.RawMessage(`{
		"provider": "kis",
		"selector_kind": "watchlist",
		"venue": "KRX",
		"market": "KR",
		"symbols": ["005930", "000660"],
		"from": "20240527",
		"to": "20240528"
	}`)

	job := Job{ID: "job-import", Kind: KindImportDailyBars, Payload: payload}
	if err := runner.Execute(context.Background(), job); err != nil {
		t.Fatalf("execute import job: %v", err)
	}

	if !imp.called {
		t.Fatal("expected importer to be dispatched, but it was not")
	}
	// provider/from/to must survive the payload -> request conversion.
	if imp.request.Provider != market.ProviderKIS {
		t.Errorf("request provider: got %q, want %q", imp.request.Provider, market.ProviderKIS)
	}
	if imp.request.Selector.Kind != market.UniverseSelectorWatchlist {
		t.Errorf("selector kind: got %q, want %q", imp.request.Selector.Kind, market.UniverseSelectorWatchlist)
	}
	if imp.request.Selector.Venue != market.VenueKRX {
		t.Errorf("selector venue: got %q, want %q", imp.request.Selector.Venue, market.VenueKRX)
	}
	if len(imp.request.Selector.Symbols) != 2 {
		t.Errorf("selector symbols: got %d, want 2", len(imp.request.Selector.Symbols))
	}
	wantFrom := time.Date(2024, 5, 27, 0, 0, 0, 0, time.UTC)
	wantTo := time.Date(2024, 5, 28, 0, 0, 0, 0, time.UTC)
	if !imp.request.From.Equal(wantFrom) {
		t.Errorf("request from: got %s, want %s", imp.request.From, wantFrom)
	}
	if !imp.request.To.Equal(wantTo) {
		t.Errorf("request to: got %s, want %s", imp.request.To, wantTo)
	}
}

func TestRegisterDailyBarImportHandlerDispatchesUSWatchlist(t *testing.T) {
	runner := NewRunner()
	imp := &stubImporter{result: importer.Result{Instruments: 1, Bars: 2}}
	RegisterDailyBarImportHandler(runner, kisCapability(), imp)

	payload := json.RawMessage(`{
		"provider": "kis",
		"selector_kind": "watchlist",
		"venue": "NASDAQ",
		"market": "US",
		"symbols": ["AAPL"],
		"from": "20240527",
		"to": "20240528"
	}`)

	job := Job{ID: "job-import-us", Kind: KindImportDailyBars, Payload: payload}
	if err := runner.Execute(context.Background(), job); err != nil {
		t.Fatalf("execute US import job: %v", err)
	}
	if !imp.called {
		t.Fatal("expected importer to be dispatched for US watchlist, but it was not")
	}
	if imp.request.Selector.Market != market.MarketUS || imp.request.Selector.Venue != market.VenueNASDAQ {
		t.Errorf("selector market/venue: got %q/%q, want US/NASDAQ", imp.request.Selector.Market, imp.request.Selector.Venue)
	}
}

func TestRegisterDailyBarImportHandlerRejectsProviderMismatch(t *testing.T) {
	runner := NewRunner()
	imp := &stubImporter{}
	RegisterDailyBarImportHandler(runner, kisCapability(), imp)

	// Missing provider is rejected at decode.
	missing := Job{ID: "j", Kind: KindImportDailyBars, Payload: json.RawMessage(`{"selector_kind":"watchlist","symbols":["005930"]}`)}
	if err := runner.Execute(context.Background(), missing); err == nil {
		t.Error("expected error for missing provider, got nil")
	}

	// A provider that does not match the capability provider is rejected.
	mismatch := Job{ID: "j", Kind: KindImportDailyBars, Payload: json.RawMessage(`{"provider":"yahoo","selector_kind":"watchlist","venue":"KRX","market":"KR","symbols":["005930"]}`)}
	if err := runner.Execute(context.Background(), mismatch); err == nil {
		t.Error("expected error for provider mismatch, got nil")
	}

	// An invalid date is rejected before dispatch (payload is otherwise valid).
	badDate := Job{ID: "j", Kind: KindImportDailyBars, Payload: json.RawMessage(`{"provider":"kis","selector_kind":"watchlist","venue":"KRX","market":"KR","symbols":["005930"],"from":"2024-05-27"}`)}
	if err := runner.Execute(context.Background(), badDate); err == nil {
		t.Error("expected error for invalid from date, got nil")
	}

	if imp.called {
		t.Error("importer must not be dispatched for rejected payloads")
	}
}

func TestRegisterDailyBarImportHandlerRejectsUnsupportedVenue(t *testing.T) {
	// A KR-only capability must reject a US venue without dispatching, and a KIS
	// capability must reject an out-of-range venue.
	cases := []struct {
		name    string
		cap     market.ProviderCapability
		payload string
	}{
		{
			name:    "kr-only capability rejects US venue",
			cap:     krOnlyCapability(),
			payload: `{"provider":"kiwoom","selector_kind":"watchlist","venue":"NASDAQ","market":"US","symbols":["AAPL"]}`,
		},
		{
			name:    "kis capability rejects unknown venue",
			cap:     kisCapability(),
			payload: `{"provider":"kis","selector_kind":"watchlist","venue":"LSE","market":"US","symbols":["AAPL"]}`,
		},
		{
			name:    "kis capability rejects market/venue mismatch",
			cap:     kisCapability(),
			payload: `{"provider":"kis","selector_kind":"watchlist","venue":"NASDAQ","market":"KR","symbols":["AAPL"]}`,
		},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			runner := NewRunner()
			imp := &stubImporter{}
			RegisterDailyBarImportHandler(runner, c.cap, imp)
			job := Job{ID: "j", Kind: KindImportDailyBars, Payload: json.RawMessage(c.payload)}
			if err := runner.Execute(context.Background(), job); err == nil {
				t.Error("expected error for unsupported venue, got nil")
			}
			if imp.called {
				t.Error("importer must not be dispatched when capability rejects the request")
			}
		})
	}
}

func TestRegisterDailyBarImportHandlerOverridesBuiltinPlaceholder(t *testing.T) {
	runner := NewRunner()
	RegisterBuiltins(runner)
	imp := &stubImporter{}
	RegisterDailyBarImportHandler(runner, kisCapability(), imp)

	// Overriding an existing kind must not add a new handler entry.
	if runner.Len() != 3 {
		t.Errorf("handler count after override: got %d, want 3", runner.Len())
	}

	payload := json.RawMessage(`{"provider":"kis","selector_kind":"watchlist","venue":"KRX","market":"KR","symbols":["005930"]}`)
	job := Job{ID: "job-import", Kind: KindImportDailyBars, Payload: payload}
	if err := runner.Execute(context.Background(), job); err != nil {
		t.Fatalf("execute import job: %v", err)
	}
	if !imp.called {
		t.Fatal("expected live importer to replace placeholder handler")
	}
}

func TestDecodeDailyBarImportPayloadRejectsMissingFields(t *testing.T) {
	if _, err := DecodeDailyBarImportPayload(json.RawMessage(`{"provider":"kis","selector_kind":"watchlist"}`)); err == nil {
		t.Error("expected error for missing symbols, got nil")
	}
	if _, err := DecodeDailyBarImportPayload(json.RawMessage(`{"provider":"kis","symbols":["005930"]}`)); err == nil {
		t.Error("expected error for missing selector_kind, got nil")
	}
	if _, err := DecodeDailyBarImportPayload(json.RawMessage(`{"selector_kind":"watchlist","symbols":["005930"]}`)); err == nil {
		t.Error("expected error for missing provider, got nil")
	}
}
