package jobs

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"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 and US/NASDAQ, NYSE equity and ETF daily bars using the capability
// matrix.
func kisCapability() market.ProviderCapability {
	return market.ProviderCapability{
		Provider: market.ProviderKIS,
		Rules: []market.ProviderCapabilityRule{
			{
				Market: market.MarketKR,
				Venue:  market.VenueKRX,
				AssetTypes: map[market.AssetType]bool{
					market.AssetTypeEquity: true,
					market.AssetTypeETF:    true,
				},
				Timeframes: map[market.Timeframe]bool{
					market.TimeframeDaily: true,
				},
			},
			{
				Market: market.MarketUS,
				Venue:  market.VenueNASDAQ,
				AssetTypes: map[market.AssetType]bool{
					market.AssetTypeEquity: true,
					market.AssetTypeETF:    true,
				},
				Timeframes: map[market.Timeframe]bool{
					market.TimeframeDaily: true,
				},
			},
			{
				Market: market.MarketUS,
				Venue:  market.VenueNYSE,
				AssetTypes: map[market.AssetType]bool{
					market.AssetTypeEquity: true,
					market.AssetTypeETF:    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"),
		Rules: []market.ProviderCapabilityRule{
			{
				Market: market.MarketKR,
				Venue:  market.VenueKRX,
				AssetTypes: map[market.AssetType]bool{
					market.AssetTypeEquity: 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 TestRegisterDailyBarImportHandlerPreservesCapabilityReasonInError(t *testing.T) {
	// Rejected capability checks must surface the reason in the error message.
	cases := []struct {
		name    string
		cap     market.ProviderCapability
		payload string
	}{
		{
			name:    "unknown venue error contains capability reason",
			cap:     kisCapability(),
			payload: `{"provider":"kis","selector_kind":"watchlist","venue":"LSE","market":"US","symbols":["AAPL"]}`,
		},
		{
			name:    "market/venue mismatch error contains capability reason",
			cap:     kisCapability(),
			payload: `{"provider":"kis","selector_kind":"watchlist","venue":"NASDAQ","market":"KR","symbols":["AAPL"]}`,
		},
		{
			name:    "provider mismatch error contains capability reason",
			cap:     kisCapability(),
			payload: `{"provider":"yahoo","selector_kind":"watchlist","venue":"KRX","market":"KR","symbols":["005930"]}`,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			runner := NewRunner()
			imp := &stubImporter{}
			RegisterDailyBarImportHandler(runner, tc.cap, imp)
			job := Job{ID: "j", Kind: KindImportDailyBars, Payload: json.RawMessage(tc.payload)}
			err := runner.Execute(context.Background(), job)
			if err == nil {
				t.Fatal("expected error, got nil")
			}
			if imp.called {
				t.Error("importer must not be dispatched for rejected payloads")
			}
			errMsg := err.Error()
			if !strings.Contains(errMsg, "daily bar import payload:") {
				t.Errorf("error missing 'daily bar import payload:' prefix: %s", errMsg)
			}
		})
	}
}

func TestRegisterDailyBarImportHandlerRejectsTimeframeViaCapabilityMatrix(t *testing.T) {
	// The job handler always uses TimeframeDaily, so daily should be accepted
	// for KIS. Create a hypothetical capability that does NOT support daily
	// and verify the job handler rejects it.
	cap := market.ProviderCapability{
		Provider: market.ProviderKIS,
		Rules: []market.ProviderCapabilityRule{
			{
				Market: market.MarketKR,
				Venue:  market.VenueKRX,
				AssetTypes: map[market.AssetType]bool{
					market.AssetTypeEquity: true,
				},
				Timeframes: map[market.Timeframe]bool{
					// No daily support - only minute is declared (unrealistic but tests the gate).
					market.TimeframeMin1: true,
				},
			},
		},
	}

	runner := NewRunner()
	imp := &stubImporter{}
	RegisterDailyBarImportHandler(runner, cap, imp)

	payload := json.RawMessage(`{"provider":"kis","selector_kind":"watchlist","venue":"KRX","market":"KR","symbols":["005930"]}`)
	job := Job{ID: "j", Kind: KindImportDailyBars, Payload: payload}

	err := runner.Execute(context.Background(), job)
	if err == nil {
		t.Error("expected rejection for KIS capability without daily support, got nil")
	}
	if imp.called {
		t.Error("importer must not be dispatched when capability rejects daily")
	}
	if !strings.Contains(err.Error(), "daily bar import payload:") {
		t.Errorf("error missing 'daily bar import payload:' prefix: %s", err.Error())
	}
	if !strings.Contains(err.Error(), "does not support") {
		t.Errorf("error missing capability reason: %s", err.Error())
	}
}

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")
	}
}

// TestDailyBarImportPayloadWithoutMarketDispatchesImporter is a regression test
// for REVIEW_API-1: the market field must be optional and the job handler must
// dispatch the importer when only venue is provided.
func TestDailyBarImportPayloadWithoutMarketDispatchesImporter(t *testing.T) {
	runner := NewRunner()
	imp := &stubImporter{result: importer.Result{Instruments: 1, Bars: 2}}
	RegisterDailyBarImportHandler(runner, kisCapability(), imp)

	// Payload without "market"; only venue is specified.
	payload := json.RawMessage(`{
		"provider": "kis",
		"selector_kind": "watchlist",
		"venue": "KRX",
		"symbols": ["005930", "000660"],
		"from": "20240527",
		"to": "20240528"
	}`)

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

	if !imp.called {
		t.Fatal("expected importer to be dispatched for no-market payload, but it was not")
	}
	if imp.request.Selector.Venue != market.VenueKRX {
		t.Errorf("selector venue: got %q, want %q", imp.request.Selector.Venue, market.VenueKRX)
	}
	// Market should remain empty after the request conversion since the payload
	// did not include it.
	if imp.request.Selector.Market != market.Market("") {
		t.Errorf("selector market: got %q, want empty", imp.request.Selector.Market)
	}
}

func TestDecodeDailyBarImportPayloadRejectsUnsupportedTimeframe(t *testing.T) {
	// Valid empty timeframe should pass.
	if _, err := DecodeDailyBarImportPayload(json.RawMessage(`{"provider":"kis","selector_kind":"watchlist","symbols":["005930"],"timeframe":""}`)); err != nil {
		t.Fatalf("expected empty timeframe to pass: %v", err)
	}

	// Valid daily timeframe should pass.
	if _, err := DecodeDailyBarImportPayload(json.RawMessage(`{"provider":"kis","selector_kind":"watchlist","symbols":["005930"],"timeframe":"daily"}`)); err != nil {
		t.Fatalf("expected daily timeframe to pass: %v", err)
	}

	// Invalid timeframe should fail — test each baseline minute value.
	baselineMinuteValues := []string{"1m", "5m", "minute_1", "minute_5", "1h"}
	for _, tf := range baselineMinuteValues {
		if _, err := DecodeDailyBarImportPayload(json.RawMessage(fmt.Sprintf(`{"provider":"kis","selector_kind":"watchlist","symbols":["005930"],"timeframe":"%s"}`, tf))); err == nil {
			t.Fatalf("expected error for unsupported timeframe %q, got nil", tf)
		}
	}
}

func TestImportTimeframeDefaultsToDaily(t *testing.T) {
	tf, err := importTimeframe("")
	if err != nil {
		t.Fatalf("expected empty timeframe to default to daily, got error: %v", err)
	}
	if tf != market.TimeframeDaily {
		t.Errorf("timeframe = %q, want %q", tf, market.TimeframeDaily)
	}

	tf, err = importTimeframe("daily")
	if err != nil {
		t.Fatalf("expected daily timeframe to pass, got error: %v", err)
	}
	if tf != market.TimeframeDaily {
		t.Errorf("timeframe = %q, want %q", tf, market.TimeframeDaily)
	}
}

func TestImportTimeframeRejectsNonDaily(t *testing.T) {
	_, err := importTimeframe("minute")
	if err == nil {
		t.Fatal("expected error for non-daily timeframe, got nil")
	}
	if !strings.Contains(err.Error(), "unsupported timeframe") {
		t.Errorf("error missing 'unsupported timeframe': %s", err.Error())
	}
}

// TestImportTimeframeRejectsAllBaselineMinuteValues verifies that every baseline
// minute timeframe vocabulary value is rejected by importTimeframe.
func TestImportTimeframeRejectsAllBaselineMinuteValues(t *testing.T) {
	baselineMinuteValues := []string{"1m", "5m", "minute_1", "minute_5"}
	for _, tf := range baselineMinuteValues {
		_, err := importTimeframe(tf)
		if err == nil {
			t.Fatalf("expected error for baseline minute timeframe %q, got nil", tf)
		}
		if !strings.Contains(err.Error(), "unsupported timeframe") {
			t.Errorf("error for %q missing 'unsupported timeframe': %s", tf, err.Error())
		}
	}
}

// TestRegisterDailyBarImportHandlerRejectsAllBaselineTimeframes is a table-driven
// handler test that verifies all baseline minute timeframe values (1m, 5m,
// minute_1, minute_5) are rejected and the importer is NOT called.
func TestRegisterDailyBarImportHandlerRejectsAllBaselineTimeframes(t *testing.T) {
	baselineMinuteValues := []string{"1m", "5m", "minute_1", "minute_5"}
	for _, tf := range baselineMinuteValues {
		t.Run(tf, func(t *testing.T) {
			runner := NewRunner()
			imp := &stubImporter{}
			RegisterDailyBarImportHandler(runner, kisCapability(), imp)

			payload := json.RawMessage(fmt.Sprintf(`{"provider":"kis","selector_kind":"watchlist","venue":"KRX","market":"KR","symbols":["005930"],"timeframe":"%s"}`, tf))
			job := Job{ID: "j", Kind: KindImportDailyBars, Payload: payload}
			err := runner.Execute(context.Background(), job)
			if err == nil {
				t.Fatal("expected error for baseline minute timeframe, got nil")
			}
			if imp.called {
				t.Error("importer must not be dispatched when timeframe is rejected")
			}
			if !strings.Contains(err.Error(), "unsupported timeframe") {
				t.Errorf("error missing 'unsupported timeframe': %s", err.Error())
			}
		})
	}
}

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

	// Explicit minute timeframe should be rejected before capability check.
	payload := json.RawMessage(`{"provider":"kis","selector_kind":"watchlist","venue":"KRX","market":"KR","symbols":["005930"],"timeframe":"minute"}`)
	job := Job{ID: "j", Kind: KindImportDailyBars, Payload: payload}
	err := runner.Execute(context.Background(), job)
	if err == nil {
		t.Fatal("expected error for minute timeframe, got nil")
	}
	if imp.called {
		t.Error("importer must not be dispatched when timeframe is rejected")
	}
	if !strings.Contains(err.Error(), "unsupported timeframe") {
		t.Errorf("error missing 'unsupported timeframe': %s", err.Error())
	}
}

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

	// Explicit daily timeframe should pass.
	payload := json.RawMessage(`{"provider":"kis","selector_kind":"watchlist","venue":"KRX","market":"KR","symbols":["005930"],"timeframe":"daily"}`)
	job := Job{ID: "j", Kind: KindImportDailyBars, Payload: payload}
	if err := runner.Execute(context.Background(), job); err != nil {
		t.Fatalf("expected daily timeframe to pass: %v", err)
	}
	if !imp.called {
		t.Fatal("expected importer to be dispatched for daily timeframe")
	}
}

func TestDailyBarImportPayloadWithoutMarketUS(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",
		"symbols": ["AAPL"],
		"from": "20240527"
	}`)

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

	if !imp.called {
		t.Fatal("expected importer to be dispatched for US no-market payload, but it was not")
	}
	if imp.request.Selector.Venue != market.VenueNASDAQ {
		t.Errorf("selector venue: got %q, want %q", imp.request.Selector.Venue, market.VenueNASDAQ)
	}
}

// --- ExecuteWithResult result path tests for RegisterDailyBarImportHandler ---

func TestRegisterDailyBarImportHandlerExecuteWithResultSuccess(t *testing.T) {
	runner := NewRunner()
	imp := &stubImporter{result: importer.Result{Instruments: 2, Bars: 10}}
	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: "ewr-daily", Kind: KindImportDailyBars, Payload: payload}

	res, hasResult, err := runner.ExecuteWithResult(context.Background(), job)
	if err != nil {
		t.Fatalf("ExecuteWithResult: unexpected error: %v", err)
	}
	if !hasResult {
		t.Fatal("ExecuteWithResult: expected hasResult=true, got false")
	}
	if res.Bars != 10 || res.Instruments != 2 {
		t.Errorf("ExecuteWithResult: expected {Bars:10,Instruments:2}, got %+v", res)
	}
}

func TestRegisterDailyBarImportHandlerExecuteWithResultError(t *testing.T) {
	runner := NewRunner()
	imp := &errorImporter{err: fmt.Errorf("import failed: connection reset")}
	RegisterDailyBarImportHandler(runner, kisCapability(), imp)

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

	job := Job{ID: "ewr-daily-err", Kind: KindImportDailyBars, Payload: payload}

	res, hasResult, err := runner.ExecuteWithResult(context.Background(), job)
	if err == nil {
		t.Fatal("ExecuteWithResult: expected error from importer, got nil")
	}
	if !strings.Contains(err.Error(), "import daily bars job:") {
		t.Errorf("ExecuteWithResult: expected 'import daily bars job:' prefix in error: %s", err.Error())
	}
	if hasResult {
		t.Fatal("ExecuteWithResult: expected hasResult=false on error, got true")
	}
	if res.Bars != 0 {
		t.Errorf("ExecuteWithResult: expected zero result on error, got %+v", res)
	}
}

// errorImporter always returns an error.
type errorImporter struct {
	err error
}

func (e *errorImporter) ImportDailyBars(_ context.Context, _ importer.DailyBarRequest) (importer.Result, error) {
	return importer.Result{}, e.err
}
