package kis

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"strings"
	"testing"
	"time"

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

func TestAuthRequestsTokenAndParsesExpiry(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != tokenPath {
			t.Fatalf("unexpected path: got %q", r.URL.Path)
		}
		if r.Method != http.MethodPost {
			t.Fatalf("unexpected method: got %q", r.Method)
		}
		var payload map[string]string
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			t.Fatalf("decode auth payload: %v", err)
		}
		if payload["grant_type"] != "client_credentials" {
			t.Errorf("grant_type: got %q", payload["grant_type"])
		}
		if payload["appkey"] != "app-key" || payload["appsecret"] != "app-secret" {
			t.Errorf("auth payload did not carry configured credentials")
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"access_token":"token-value","access_token_token_expired":"2026-06-04 12:34:56"}`))
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, AppKey: "app-key", AppSecret: "app-secret"})
	token, err := client.Auth(context.Background())
	if err != nil {
		t.Fatalf("auth: %v", err)
	}
	if token.AccessToken != "token-value" {
		t.Errorf("access token: got %q", token.AccessToken)
	}
	if token.ExpiresAt.IsZero() {
		t.Error("expected parsed expiry, got zero time")
	}
}

func TestAuthUnavailableWhenCredentialsAreMissing(t *testing.T) {
	client := NewClient(Config{BaseURL: "https://example.invalid", AppKey: "", AppSecret: ""})
	_, err := client.Auth(context.Background())
	if err == nil {
		t.Fatal("expected unavailable error, got nil")
	}
	if !IsErrorKind(err, ErrorUnavailable) {
		t.Fatalf("error kind: got %v, want %s", err, ErrorUnavailable)
	}
}

func TestConfigFromEnvSelectsExplicitEnvironment(t *testing.T) {
	t.Run("paper", func(t *testing.T) {
		t.Setenv("KIS_ACTIVE_ENV", "paper")
		t.Setenv("KIS_PAPER_APP_KEY", "paper-key")
		t.Setenv("KIS_PAPER_APP_SECRET", "paper-secret")
		t.Setenv("KIS_PAPER_CANO", "paper-cano")
		t.Setenv("KIS_PAPER_ACNT_PRDT_CD", "01")

		cfg := ConfigFromEnv()
		if cfg.ConfigError != "" {
			t.Fatalf("config error: %s", cfg.ConfigError)
		}
		if cfg.Environment != EnvironmentPaper {
			t.Fatalf("environment: got %q, want %q", cfg.Environment, EnvironmentPaper)
		}
		if cfg.BaseURL != PaperBaseURL || cfg.AppKey != "paper-key" || cfg.AppSecret != "paper-secret" {
			t.Fatalf("paper config: got %+v", cfg)
		}
	})

	t.Run("real", func(t *testing.T) {
		t.Setenv("KIS_ACTIVE_ENV", "real")
		t.Setenv("KIS_REAL_APP_KEY", "real-key")
		t.Setenv("KIS_REAL_APP_SECRET", "real-secret")
		t.Setenv("KIS_REAL_CANO", "real-cano")
		t.Setenv("KIS_REAL_ACNT_PRDT_CD", "01")

		cfg := ConfigFromEnv()
		if cfg.ConfigError != "" {
			t.Fatalf("config error: %s", cfg.ConfigError)
		}
		if cfg.Environment != EnvironmentReal {
			t.Fatalf("environment: got %q, want %q", cfg.Environment, EnvironmentReal)
		}
		if cfg.BaseURL != RealBaseURL || cfg.AppKey != "real-key" || cfg.AppSecret != "real-secret" {
			t.Fatalf("real config: got %+v", cfg)
		}
	})
}

func TestConfigFromEnvRejectsMissingOrUnknownEnvironment(t *testing.T) {
	for _, activeEnv := range []string{"", "papre"} {
		t.Run("env="+activeEnv, func(t *testing.T) {
			t.Setenv("KIS_ACTIVE_ENV", activeEnv)
			client := NewClient(ConfigFromEnv())
			_, err := client.Auth(context.Background())
			if err == nil {
				t.Fatal("expected unavailable error, got nil")
			}
			if !IsErrorKind(err, ErrorUnavailable) {
				t.Fatalf("error kind: got %v, want %s", err, ErrorUnavailable)
			}
			if !strings.Contains(err.Error(), "KIS_ACTIVE_ENV") {
				t.Fatalf("error should mention KIS_ACTIVE_ENV: %v", err)
			}
		})
	}
}

func TestInquireDailyItemChartPriceSendsHeadersAndDecodes(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != DailyItemChartPricePath {
			t.Fatalf("unexpected path: got %q", r.URL.Path)
		}
		if got := r.Header.Get("authorization"); got != "Bearer access-token" {
			t.Errorf("authorization header: got %q", got)
		}
		if got := r.Header.Get("tr_id"); got != DailyItemChartPriceTRID {
			t.Errorf("tr_id header: got %q", got)
		}
		if got := r.Header.Get("custtype"); got != "P" {
			t.Errorf("custtype header: got %q", got)
		}
		query := r.URL.Query()
		wantParams := map[string]string{
			"FID_COND_MRKT_DIV_CODE": "J",
			"FID_INPUT_ISCD":         "005930",
			"FID_INPUT_DATE_1":       "20240527",
			"FID_INPUT_DATE_2":       "20240528",
			"FID_PERIOD_DIV_CODE":    "D",
			"FID_ORG_ADJ_PRC":        "0",
		}
		for key, want := range wantParams {
			if got := query.Get(key); got != want {
				t.Errorf("query %s: got %q, want %q", key, got, want)
			}
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write(readFixture(t, "daily_itemchartprice_response.sample.json"))
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, AppKey: "app-key", AppSecret: "app-secret"})
	resp, err := client.InquireDailyItemChartPrice(context.Background(), "access-token", DailyItemChartPriceQuery{
		Symbol: "005930",
		From:   "20240527",
		To:     "20240528",
	})
	if err != nil {
		t.Fatalf("daily query: %v", err)
	}
	if resp.Output1.ShortCode != "005930" {
		t.Errorf("short code: got %q", resp.Output1.ShortCode)
	}
	if len(resp.Output2) != 2 {
		t.Errorf("bar rows: got %d, want 2", len(resp.Output2))
	}
}

func TestInquireDailyItemChartPriceClassifiesQuotaAndMalformed(t *testing.T) {
	t.Run("quota", func(t *testing.T) {
		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
			w.WriteHeader(http.StatusTooManyRequests)
			_, _ = w.Write([]byte(`{"msg_cd":"RATE_LIMIT","msg1":"too many requests"}`))
		}))
		defer server.Close()

		client := NewClient(Config{BaseURL: server.URL, AppKey: "app-key", AppSecret: "app-secret"})
		_, err := client.InquireDailyItemChartPrice(context.Background(), "access-token", DailyItemChartPriceQuery{
			Symbol: "005930",
			From:   "20240527",
			To:     "20240528",
		})
		if err == nil || !IsErrorKind(err, ErrorQuota) {
			t.Fatalf("error kind: got %v, want %s", err, ErrorQuota)
		}
	})

	t.Run("malformed", func(t *testing.T) {
		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
			_, _ = w.Write([]byte(`{`))
		}))
		defer server.Close()

		client := NewClient(Config{BaseURL: server.URL, AppKey: "app-key", AppSecret: "app-secret"})
		_, err := client.InquireDailyItemChartPrice(context.Background(), "access-token", DailyItemChartPriceQuery{
			Symbol: "005930",
			From:   "20240527",
			To:     "20240528",
		})
		if err == nil || !IsErrorKind(err, ErrorMalformed) {
			t.Fatalf("error kind: got %v, want %s", err, ErrorMalformed)
		}
	})
}

func TestInquireDailyItemChartPriceDoesNotExposeSecretsInError(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusForbidden)
		_, _ = w.Write([]byte(`{"msg_cd":"AUTH","msg1":"invalid credential"}`))
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, AppKey: "app-key", AppSecret: "app-secret"})
	_, err := client.InquireDailyItemChartPrice(context.Background(), "access-token", DailyItemChartPriceQuery{
		Symbol: "005930",
		From:   "20240527",
		To:     "20240528",
	})
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	text := err.Error()
	for _, secret := range []string{"app-key", "app-secret", "access-token"} {
		if strings.Contains(text, secret) {
			t.Fatalf("error exposed secret %q: %s", secret, text)
		}
	}
}

func TestLiveProviderFetchesAndNormalizesDailyBars(t *testing.T) {
	var authCalls, dailyCalls int
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case tokenPath:
			authCalls++
			_, _ = w.Write([]byte(`{"access_token":"access-token","access_token_token_expired":"2026-06-04 12:34:56"}`))
		case DailyItemChartPricePath:
			dailyCalls++
			if got := r.URL.Query().Get("FID_INPUT_ISCD"); got != "005930" {
				t.Errorf("symbol query: got %q", got)
			}
			_, _ = w.Write(readFixture(t, "daily_itemchartprice_response.sample.json"))
		default:
			t.Fatalf("unexpected path: %s", r.URL.Path)
		}
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, AppKey: "app-key", AppSecret: "app-secret"})
	provider := NewLiveProvider(client)
	seoul := time.FixedZone("Asia/Seoul", 9*60*60)
	items, err := provider.FetchDailyBars(context.Background(), importer.DailyBarRequest{
		Provider: market.ProviderKIS,
		Selector: market.UniverseSelector{
			Kind:    market.UniverseSelectorWatchlist,
			Venue:   market.VenueKRX,
			Symbols: []string{"005930"},
		},
		From: time.Date(2024, 5, 27, 0, 0, 0, 0, seoul),
		To:   time.Date(2024, 5, 28, 0, 0, 0, 0, seoul),
	})
	if err != nil {
		t.Fatalf("fetch daily bars: %v", err)
	}
	if authCalls != 1 || dailyCalls != 1 {
		t.Fatalf("calls: auth=%d daily=%d, want 1 each", authCalls, dailyCalls)
	}
	if len(items) != 1 {
		t.Fatalf("items: got %d, want 1", len(items))
	}
	if items[0].Instrument.ID != market.InstrumentID("KRX:005930") {
		t.Errorf("instrument id: got %q", items[0].Instrument.ID)
	}
	if got := items[0].Instrument.ProviderSymbols[string(market.ProviderKIS)]; got != "005930" {
		t.Errorf("provider symbol: got %q", got)
	}
	if len(items[0].Bars) != 2 {
		t.Fatalf("bars: got %d, want 2", len(items[0].Bars))
	}
	if items[0].Bars[0].InstrumentID != items[0].Instrument.ID {
		t.Errorf("bar instrument id: got %q, want %q", items[0].Bars[0].InstrumentID, items[0].Instrument.ID)
	}
}

func TestInquireOverseasDailyPriceSendsHeadersAndDecodes(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != OverseasDailyPricePath {
			t.Fatalf("unexpected path: got %q", r.URL.Path)
		}
		if got := r.Header.Get("authorization"); got != "Bearer access-token" {
			t.Errorf("authorization header: got %q", got)
		}
		if got := r.Header.Get("tr_id"); got != OverseasDailyPriceTRID {
			t.Errorf("tr_id header: got %q", got)
		}
		if got := r.Header.Get("custtype"); got != "P" {
			t.Errorf("custtype header: got %q", got)
		}
		query := r.URL.Query()
		wantParams := map[string]string{
			"EXCD": "NAS",
			"SYMB": "AAPL",
			"GUBN": "0",
			"BYMD": "20240529",
			"MODP": "1",
		}
		for key, want := range wantParams {
			if got := query.Get(key); got != want {
				t.Errorf("query %s: got %q, want %q", key, got, want)
			}
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write(readFixture(t, "overseas_dailyprice_response.sample.json"))
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, AppKey: "app-key", AppSecret: "app-secret"})
	resp, err := client.InquireOverseasDailyPrice(context.Background(), "access-token", OverseasDailyPriceQuery{
		ExchangeCode: "NAS",
		Symbol:       "AAPL",
		BaseDate:     "20240529",
	})
	if err != nil {
		t.Fatalf("overseas query: %v", err)
	}
	if len(resp.Output2) != 2 {
		t.Errorf("bar rows: got %d, want 2", len(resp.Output2))
	}
	// Lock the overseas field rename the decoder must read.
	if resp.Output2[0].BusinessDate != "20240528" || resp.Output2[0].Close != "189.99" {
		t.Errorf("overseas row decode: got date=%q clos=%q", resp.Output2[0].BusinessDate, resp.Output2[0].Close)
	}
}

func TestLiveProviderFetchesAndNormalizesOverseasDailyBars(t *testing.T) {
	var authCalls, overseasCalls int
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case tokenPath:
			authCalls++
			_, _ = w.Write([]byte(`{"access_token":"access-token","access_token_token_expired":"2026-06-04 12:34:56"}`))
		case OverseasDailyPricePath:
			overseasCalls++
			if got := r.URL.Query().Get("SYMB"); got != "AAPL" {
				t.Errorf("symbol query: got %q", got)
			}
			if got := r.URL.Query().Get("EXCD"); got != "NAS" {
				t.Errorf("exchange query: got %q", got)
			}
			_, _ = w.Write(readFixture(t, "overseas_dailyprice_response.sample.json"))
		case DailyItemChartPricePath:
			t.Fatal("US request must not hit the domestic endpoint")
		default:
			t.Fatalf("unexpected path: %s", r.URL.Path)
		}
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, AppKey: "app-key", AppSecret: "app-secret"})
	provider := NewLiveProvider(client)
	ny := time.FixedZone("America/New_York", -4*60*60)
	items, err := provider.FetchDailyBars(context.Background(), importer.DailyBarRequest{
		Provider: market.ProviderKIS,
		Selector: market.UniverseSelector{
			Kind:    market.UniverseSelectorWatchlist,
			Market:  market.MarketUS,
			Venue:   market.VenueNASDAQ,
			Symbols: []string{"AAPL"},
		},
		From: time.Date(2024, 5, 28, 0, 0, 0, 0, ny),
		To:   time.Date(2024, 5, 29, 0, 0, 0, 0, ny),
	})
	if err != nil {
		t.Fatalf("fetch overseas daily bars: %v", err)
	}
	if authCalls != 1 || overseasCalls != 1 {
		t.Fatalf("calls: auth=%d overseas=%d, want 1 each", authCalls, overseasCalls)
	}
	if len(items) != 1 {
		t.Fatalf("items: got %d, want 1", len(items))
	}
	inst := items[0].Instrument
	if inst.ID != market.InstrumentID("NASDAQ:AAPL") {
		t.Errorf("instrument id: got %q, want NASDAQ:AAPL", inst.ID)
	}
	if inst.Market != market.MarketUS || inst.Venue != market.VenueNASDAQ {
		t.Errorf("instrument market/venue: got %q/%q, want US/NASDAQ", inst.Market, inst.Venue)
	}
	if inst.Currency != market.CurrencyUSD {
		t.Errorf("instrument currency: got %q, want USD", inst.Currency)
	}
	if got := inst.ProviderSymbols[string(market.ProviderKIS)]; got != "AAPL" {
		t.Errorf("provider symbol: got %q, want AAPL", got)
	}
	if len(items[0].Bars) != 2 {
		t.Fatalf("bars: got %d, want 2", len(items[0].Bars))
	}
	bar := items[0].Bars[0]
	if bar.Close.Currency != market.CurrencyUSD {
		t.Errorf("bar currency: got %q, want USD", bar.Close.Currency)
	}
	if got := bar.Timestamp.Format(time.RFC3339); got != "2024-05-28T00:00:00-04:00" {
		t.Errorf("bar timestamp: got %q, want New York midnight", got)
	}
}

func TestLiveProviderFiltersOverseasDailyBarsToRequestedRange(t *testing.T) {
	var authCalls, overseasCalls int
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case tokenPath:
			authCalls++
			_, _ = w.Write([]byte(`{"access_token":"access-token","access_token_token_expired":"2026-06-04 12:34:56"}`))
		case OverseasDailyPricePath:
			overseasCalls++
			if got := r.URL.Query().Get("SYMB"); got != "AAPL" {
				t.Errorf("symbol query: got %q", got)
			}
			if got := r.URL.Query().Get("EXCD"); got != "NAS" {
				t.Errorf("exchange query: got %q", got)
			}
			var resp OverseasDailyPriceResponse
			fixtureData := readFixture(t, "overseas_dailyprice_response.sample.json")
			if err := json.Unmarshal(fixtureData, &resp); err != nil {
				t.Fatalf("unmarshal fixture: %v", err)
			}
			resp.Output2 = append(resp.Output2, OverseasDailyPriceBarRow{
				BusinessDate: "20240527",
				Open:         "188.00",
				High:         "189.00",
				Low:          "187.00",
				Close:        "188.50",
				Volume:       "40000000",
				TradeAmount:  "7500000000",
			})
			w.Header().Set("Content-Type", "application/json")
			if err := json.NewEncoder(w).Encode(resp); err != nil {
				t.Fatalf("encode mock response: %v", err)
			}
		case DailyItemChartPricePath:
			t.Fatal("US request must not hit the domestic endpoint")
		default:
			t.Fatalf("unexpected path: %s", r.URL.Path)
		}
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, AppKey: "app-key", AppSecret: "app-secret"})
	provider := NewLiveProvider(client)
	ny := time.FixedZone("America/New_York", -4*60*60)
	items, err := provider.FetchDailyBars(context.Background(), importer.DailyBarRequest{
		Provider: market.ProviderKIS,
		Selector: market.UniverseSelector{
			Kind:    market.UniverseSelectorWatchlist,
			Market:  market.MarketUS,
			Venue:   market.VenueNASDAQ,
			Symbols: []string{"AAPL"},
		},
		From: time.Date(2024, 5, 28, 0, 0, 0, 0, ny),
		To:   time.Date(2024, 5, 29, 0, 0, 0, 0, ny),
	})
	if err != nil {
		t.Fatalf("fetch overseas daily bars: %v", err)
	}
	if authCalls != 1 || overseasCalls != 1 {
		t.Fatalf("calls: auth=%d overseas=%d, want 1 each", authCalls, overseasCalls)
	}
	if len(items) != 1 {
		t.Fatalf("items: got %d, want 1", len(items))
	}
	if len(items[0].Bars) != 2 {
		t.Fatalf("bars: got %d, want 2", len(items[0].Bars))
	}
	for _, bar := range items[0].Bars {
		barDate := bar.Timestamp.Format("20060102")
		if barDate != "20240528" && barDate != "20240529" {
			t.Errorf("unexpected bar date stored: %s", barDate)
		}
	}
}

func TestLiveProviderRejectsMissingRange(t *testing.T) {
	provider := NewLiveProvider(NewClient(Config{BaseURL: "https://example.invalid", AppKey: "app-key", AppSecret: "app-secret"}))
	_, err := provider.FetchDailyBars(context.Background(), importer.DailyBarRequest{
		Provider: market.ProviderKIS,
		Selector: market.UniverseSelector{Symbols: []string{"005930"}},
	})
	if err == nil || !IsErrorKind(err, ErrorMalformed) {
		t.Fatalf("error kind: got %v, want %s", err, ErrorMalformed)
	}
}

type importStore struct {
	instruments map[market.InstrumentID]market.Instrument
	bars        map[importBarKey]market.Bar
}

type importBarKey struct {
	instrumentID market.InstrumentID
	timeframe    market.Timeframe
	timestamp    time.Time
}

func newImportStore() *importStore {
	return &importStore{
		instruments: make(map[market.InstrumentID]market.Instrument),
		bars:        make(map[importBarKey]market.Bar),
	}
}

func (s *importStore) UpsertInstrument(_ context.Context, inst market.Instrument) error {
	s.instruments[inst.ID] = inst
	return nil
}

func (s *importStore) GetInstrument(_ context.Context, id market.InstrumentID) (market.Instrument, error) {
	inst, ok := s.instruments[id]
	if !ok {
		return market.Instrument{}, storage.ErrInstrumentNotFound
	}
	return inst, nil
}

func (s *importStore) ListInstruments(_ context.Context) ([]market.Instrument, error) {
	out := make([]market.Instrument, 0, len(s.instruments))
	for _, inst := range s.instruments {
		out = append(out, inst)
	}
	return out, nil
}

func (s *importStore) UpsertBar(_ context.Context, bar market.Bar) error {
	s.bars[importBarKey{bar.InstrumentID, bar.Timeframe, bar.Timestamp}] = bar
	return nil
}

func (s *importStore) GetBars(_ context.Context, id market.InstrumentID, timeframe market.Timeframe, from, to time.Time) ([]market.Bar, error) {
	var out []market.Bar
	for key, bar := range s.bars {
		if key.instrumentID != id || key.timeframe != timeframe {
			continue
		}
		if key.timestamp.Before(from) || key.timestamp.After(to) {
			continue
		}
		out = append(out, bar)
	}
	return out, nil
}

func TestLiveProviderImportFeedsBacktestBarSource(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case tokenPath:
			_, _ = w.Write([]byte(`{"access_token":"access-token","access_token_token_expired":"2026-06-04 12:34:56"}`))
		case DailyItemChartPricePath:
			_, _ = w.Write(readFixture(t, "daily_itemchartprice_response.sample.json"))
		default:
			t.Fatalf("unexpected path: %s", r.URL.Path)
		}
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, AppKey: "app-key", AppSecret: "app-secret"})
	provider := NewLiveProvider(client)
	store := newImportStore()
	imp := importer.New(provider, store)

	seoul := time.FixedZone("Asia/Seoul", 9*60*60)
	from := time.Date(2024, 5, 27, 0, 0, 0, 0, seoul)
	to := time.Date(2024, 5, 28, 0, 0, 0, 0, seoul)
	result, err := imp.ImportDailyBars(context.Background(), importer.DailyBarRequest{
		Provider: market.ProviderKIS,
		Selector: market.UniverseSelector{
			Kind:    market.UniverseSelectorWatchlist,
			Venue:   market.VenueKRX,
			Symbols: []string{"005930"},
		},
		From: from,
		To:   to,
	})
	if err != nil {
		t.Fatalf("import daily bars: %v", err)
	}
	if result.Instruments != 1 || result.Bars != 2 {
		t.Fatalf("import result: got %+v, want 1 instrument and 2 bars", result)
	}

	source := workerbacktest.NewStorageBarSource(store, store)
	bars, err := source.GetBars(context.Background(), market.MarketKR, market.TimeframeDaily, from, to)
	if err != nil {
		t.Fatalf("backtest bar source query: %v", err)
	}
	if len(bars) != 2 {
		t.Fatalf("query bars: got %d, want 2", len(bars))
	}
	if bars[0].InstrumentID != market.InstrumentID("KRX:005930") {
		t.Fatalf("bar instrument id: got %q", bars[0].InstrumentID)
	}
}

func TestLiveProviderImportStoresUSDailyBars(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case tokenPath:
			_, _ = w.Write([]byte(`{"access_token":"access-token","access_token_token_expired":"2026-06-04 12:34:56"}`))
		case OverseasDailyPricePath:
			_, _ = w.Write(readFixture(t, "overseas_dailyprice_response.sample.json"))
		case DailyItemChartPricePath:
			t.Fatal("US import must not hit the domestic endpoint")
		default:
			t.Fatalf("unexpected path: %s", r.URL.Path)
		}
	}))
	defer server.Close()

	client := NewClient(Config{BaseURL: server.URL, AppKey: "app-key", AppSecret: "app-secret"})
	provider := NewLiveProvider(client)
	store := newImportStore()
	imp := importer.New(provider, store)

	ny := time.FixedZone("America/New_York", -4*60*60)
	from := time.Date(2024, 5, 28, 0, 0, 0, 0, ny)
	to := time.Date(2024, 5, 29, 0, 0, 0, 0, ny)
	result, err := imp.ImportDailyBars(context.Background(), importer.DailyBarRequest{
		Provider: market.ProviderKIS,
		Selector: market.UniverseSelector{
			Kind:    market.UniverseSelectorWatchlist,
			Market:  market.MarketUS,
			Venue:   market.VenueNASDAQ,
			Symbols: []string{"AAPL"},
		},
		From: from,
		To:   to,
	})
	if err != nil {
		t.Fatalf("import US daily bars: %v", err)
	}
	if result.Instruments != 1 || result.Bars != 2 {
		t.Fatalf("import result: got %+v, want 1 instrument and 2 bars", result)
	}

	stored, err := store.GetInstrument(context.Background(), market.InstrumentID("NASDAQ:AAPL"))
	if err != nil {
		t.Fatalf("get stored instrument: %v", err)
	}
	if stored.Market != market.MarketUS || stored.Venue != market.VenueNASDAQ || stored.Currency != market.CurrencyUSD {
		t.Errorf("stored instrument: got market=%q venue=%q currency=%q, want US/NASDAQ/USD", stored.Market, stored.Venue, stored.Currency)
	}

	bars, err := store.GetBars(context.Background(), market.InstrumentID("NASDAQ:AAPL"), market.TimeframeDaily, from.Add(-24*time.Hour), to.Add(24*time.Hour))
	if err != nil {
		t.Fatalf("get stored bars: %v", err)
	}
	if len(bars) != 2 {
		t.Fatalf("stored bars: got %d, want 2", len(bars))
	}
	for _, b := range bars {
		if b.Close.Currency != market.CurrencyUSD {
			t.Errorf("stored bar currency: got %q, want USD", b.Close.Currency)
		}
	}
}

func TestLiveProviderPaperSmoke(t *testing.T) {
	if os.Getenv("KIS_LIVE_SMOKE") != "1" {
		t.Skip("set KIS_LIVE_SMOKE=1 with SOPS-injected KIS paper env to run live smoke")
	}
	runLiveProviderSmoke(t, EnvironmentPaper)
}

func TestLiveProviderRealSmoke(t *testing.T) {
	if os.Getenv("KIS_REAL_LIVE_SMOKE") != "1" {
		t.Skip("set KIS_REAL_LIVE_SMOKE=1 with SOPS-injected KIS real env to run live smoke")
	}
	runLiveProviderSmoke(t, EnvironmentReal)
}

func runLiveProviderSmoke(t *testing.T, env Environment) {
	t.Helper()
	if os.Getenv("KIS_ACTIVE_ENV") != string(env) {
		t.Fatalf("KIS_ACTIVE_ENV must be %s for this live smoke", env)
	}
	provider := NewLiveProvider(NewClient(ConfigFromEnv()))
	seoul := time.FixedZone("Asia/Seoul", 9*60*60)
	items, err := provider.FetchDailyBars(context.Background(), importer.DailyBarRequest{
		Provider: market.ProviderKIS,
		Selector: market.UniverseSelector{
			Kind:    market.UniverseSelectorWatchlist,
			Venue:   market.VenueKRX,
			Symbols: []string{"005930"},
		},
		From: time.Date(2024, 5, 27, 0, 0, 0, 0, seoul),
		To:   time.Date(2024, 5, 28, 0, 0, 0, 0, seoul),
	})
	if err != nil {
		t.Fatalf("%s live provider smoke: %v", env, err)
	}
	if len(items) != 1 {
		t.Fatalf("items: got %d, want 1", len(items))
	}
	if items[0].Instrument.ID != market.InstrumentID("KRX:005930") {
		t.Fatalf("instrument id: got %q", items[0].Instrument.ID)
	}
	if len(items[0].Bars) == 0 {
		t.Fatal("expected at least one live bar")
	}
}
