package socket

import (
	"context"
	"errors"
	"testing"
	"time"

	altv1 "git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1"
	"git.toki-labs.com/toki/alt/packages/domain/market"
	"git.toki-labs.com/toki/alt/services/worker/internal/marketdata/importer"
	"git.toki-labs.com/toki/alt/services/worker/internal/storage"
	protoSocket "git.toki-labs.com/toki/proto-socket/go"
)

type fakeInstrumentStore struct {
	listCalled bool
	insts      []market.Instrument
	err        error
}

func (f *fakeInstrumentStore) UpsertInstrument(ctx context.Context, inst market.Instrument) error {
	return nil
}

func (f *fakeInstrumentStore) GetInstrument(ctx context.Context, id market.InstrumentID) (market.Instrument, error) {
	return market.Instrument{}, nil
}

func (f *fakeInstrumentStore) ListInstruments(ctx context.Context) ([]market.Instrument, error) {
	f.listCalled = true
	return f.insts, f.err
}

type fakeBarStore struct {
	gotID        market.InstrumentID
	gotTimeframe market.Timeframe
	gotFrom      time.Time
	gotTo        time.Time
	bars         []market.Bar
	err          error
}

func (f *fakeBarStore) UpsertBar(ctx context.Context, bar market.Bar) error { return nil }

func (f *fakeBarStore) GetBars(ctx context.Context, id market.InstrumentID, timeframe market.Timeframe, from, to time.Time) ([]market.Bar, error) {
	f.gotID = id
	f.gotTimeframe = timeframe
	f.gotFrom = from
	f.gotTo = to
	return f.bars, f.err
}

type fakeDailyBarImporter struct {
	gotReq importer.DailyBarRequest
	called bool
	result importer.Result
	err    error
}

func (f *fakeDailyBarImporter) ImportDailyBars(ctx context.Context, request importer.DailyBarRequest) (importer.Result, error) {
	f.called = true
	f.gotReq = request
	return f.result, f.err
}

func validImportRequest() *altv1.ImportDailyBarsRequest {
	return &altv1.ImportDailyBarsRequest{
		Provider:     "kis",
		SelectorKind: "watchlist",
		Market:       altv1.Market_MARKET_KR,
		Venue:        altv1.Venue_VENUE_KRX,
		Name:         "my-watchlist",
		Symbols:      []string{"005930", "000660"},
		FromYyyymmdd: "20260501",
		ToYyyymmdd:   "20260515",
	}
}

func validListBarsRequest() *altv1.ListBarsRequest {
	return &altv1.ListBarsRequest{
		InstrumentId: "KRX:005930",
		Timeframe:    altv1.Timeframe_TIMEFRAME_DAILY,
		FromUnixMs:   time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
		ToUnixMs:     time.Date(2026, 5, 15, 0, 0, 0, 0, time.UTC).UnixMilli(),
	}
}

func TestHandleListInstrumentsFiltersMarketAndProvider(t *testing.T) {
	store := &fakeInstrumentStore{insts: []market.Instrument{
		{ID: "KRX:005930", Market: market.MarketKR, Symbol: "005930", ProviderSymbols: map[string]string{"kis": "005930"}},
		{ID: "KRX:000660", Market: market.MarketKR, Symbol: "000660", ProviderSymbols: map[string]string{"other": "000660"}},
		{ID: "NASDAQ:AAPL", Market: market.MarketUS, Symbol: "AAPL", ProviderSymbols: map[string]string{"kis": "AAPL"}},
	}}

	resp, err := handleListInstruments(Deps{Instruments: store}, &altv1.ListInstrumentsRequest{
		Market:   altv1.Market_MARKET_KR,
		Provider: "kis",
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !store.listCalled {
		t.Fatal("expected instrument store to be called")
	}
	if len(resp.GetInstruments()) != 1 {
		t.Fatalf("expected one filtered instrument, got %d", len(resp.GetInstruments()))
	}
	if resp.GetInstruments()[0].GetId() != "KRX:005930" {
		t.Errorf("unexpected instrument: %+v", resp.GetInstruments()[0])
	}
}

func TestHandleListInstrumentsFiltersMarketAndProviderUS(t *testing.T) {
	store := &fakeInstrumentStore{insts: []market.Instrument{
		{ID: "KRX:005930", Market: market.MarketKR, Symbol: "005930", ProviderSymbols: map[string]string{"kis": "005930"}},
		{ID: "KRX:000660", Market: market.MarketKR, Symbol: "000660", ProviderSymbols: map[string]string{"other": "000660"}},
		{ID: "NASDAQ:AAPL", Market: market.MarketUS, Symbol: "AAPL", ProviderSymbols: map[string]string{"kis": "AAPL"}},
	}}

	resp, err := handleListInstruments(Deps{Instruments: store}, &altv1.ListInstrumentsRequest{
		Market:   altv1.Market_MARKET_US,
		Provider: "kis",
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !store.listCalled {
		t.Fatal("expected instrument store to be called")
	}
	if len(resp.GetInstruments()) != 1 {
		t.Fatalf("expected one filtered instrument, got %d", len(resp.GetInstruments()))
	}
	if resp.GetInstruments()[0].GetId() != "NASDAQ:AAPL" {
		t.Errorf("unexpected instrument: %+v", resp.GetInstruments()[0])
	}
}

func TestHandleListInstrumentsEmpty(t *testing.T) {
	resp, err := handleListInstruments(Deps{Instruments: &fakeInstrumentStore{}}, &altv1.ListInstrumentsRequest{})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(resp.GetInstruments()) != 0 {
		t.Errorf("expected empty instruments, got %d", len(resp.GetInstruments()))
	}
}

func TestHandleListInstrumentsInvalidMarket(t *testing.T) {
	store := &fakeInstrumentStore{}
	resp, err := handleListInstruments(Deps{Instruments: store}, &altv1.ListInstrumentsRequest{Market: altv1.Market(99)})
	if err != nil {
		t.Fatalf("expected typed validation response, got error: %v", err)
	}
	requireMarketError(t, resp.GetError(), marketErrorInvalidRequest)
	if store.listCalled {
		t.Error("store must not be called when validation fails")
	}
}

func TestHandleListInstrumentsUnavailable(t *testing.T) {
	resp, err := handleListInstruments(Deps{}, &altv1.ListInstrumentsRequest{})
	if err != nil {
		t.Fatalf("expected typed unavailable response, got error: %v", err)
	}
	requireMarketError(t, resp.GetError(), marketErrorUnavailable)
}

func TestHandleListInstrumentsStoreError(t *testing.T) {
	resp, err := handleListInstruments(Deps{Instruments: &fakeInstrumentStore{err: errors.New("boom")}}, &altv1.ListInstrumentsRequest{})
	if err != nil {
		t.Fatalf("expected typed internal response, got error: %v", err)
	}
	requireMarketError(t, resp.GetError(), marketErrorInternal)
}

func TestHandleListBarsSuccess(t *testing.T) {
	req := validListBarsRequest()
	store := &fakeBarStore{bars: []market.Bar{{
		InstrumentID: "KRX:005930",
		Timeframe:    market.TimeframeDaily,
		Timestamp:    time.UnixMilli(req.GetFromUnixMs()).UTC(),
	}}}

	resp, err := handleListBars(Deps{Bars: store}, req)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if store.gotID != "KRX:005930" {
		t.Errorf("instrument id mismatch: %q", store.gotID)
	}
	if store.gotTimeframe != market.TimeframeDaily {
		t.Errorf("timeframe mismatch: %q", store.gotTimeframe)
	}
	if !store.gotFrom.Equal(time.UnixMilli(req.GetFromUnixMs()).UTC()) || !store.gotTo.Equal(time.UnixMilli(req.GetToUnixMs()).UTC()) {
		t.Errorf("time range mismatch: got %v..%v", store.gotFrom, store.gotTo)
	}
	if len(resp.GetBars()) != 1 {
		t.Errorf("expected 1 bar, got %d", len(resp.GetBars()))
	}
}

func TestHandleListBarsSuccessUS(t *testing.T) {
	req := &altv1.ListBarsRequest{
		InstrumentId: "NASDAQ:AAPL",
		Timeframe:    altv1.Timeframe_TIMEFRAME_DAILY,
		FromUnixMs:   time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
		ToUnixMs:     time.Date(2026, 5, 15, 0, 0, 0, 0, time.UTC).UnixMilli(),
	}
	store := &fakeBarStore{bars: []market.Bar{{
		InstrumentID: "NASDAQ:AAPL",
		Timeframe:    market.TimeframeDaily,
		Timestamp:    time.UnixMilli(req.GetFromUnixMs()).UTC(),
	}}}

	resp, err := handleListBars(Deps{Bars: store}, req)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if store.gotID != "NASDAQ:AAPL" {
		t.Errorf("instrument id mismatch: %q", store.gotID)
	}
	if store.gotTimeframe != market.TimeframeDaily {
		t.Errorf("timeframe mismatch: %q", store.gotTimeframe)
	}
	if !store.gotFrom.Equal(time.UnixMilli(req.GetFromUnixMs()).UTC()) || !store.gotTo.Equal(time.UnixMilli(req.GetToUnixMs()).UTC()) {
		t.Errorf("time range mismatch: got %v..%v", store.gotFrom, store.gotTo)
	}
	if len(resp.GetBars()) != 1 {
		t.Errorf("expected 1 bar, got %d", len(resp.GetBars()))
	}
}

func TestHandleListBarsValidation(t *testing.T) {
	valid := validListBarsRequest()
	tests := []struct {
		name string
		req  *altv1.ListBarsRequest
	}{
		{"missing instrument", &altv1.ListBarsRequest{Timeframe: valid.GetTimeframe(), FromUnixMs: valid.GetFromUnixMs(), ToUnixMs: valid.GetToUnixMs()}},
		{"unknown timeframe", &altv1.ListBarsRequest{InstrumentId: valid.GetInstrumentId(), Timeframe: altv1.Timeframe(99), FromUnixMs: valid.GetFromUnixMs(), ToUnixMs: valid.GetToUnixMs()}},
		{"missing from", &altv1.ListBarsRequest{InstrumentId: valid.GetInstrumentId(), Timeframe: valid.GetTimeframe(), ToUnixMs: valid.GetToUnixMs()}},
		{"missing to", &altv1.ListBarsRequest{InstrumentId: valid.GetInstrumentId(), Timeframe: valid.GetTimeframe(), FromUnixMs: valid.GetFromUnixMs()}},
		{"inverted range", &altv1.ListBarsRequest{InstrumentId: valid.GetInstrumentId(), Timeframe: valid.GetTimeframe(), FromUnixMs: valid.GetToUnixMs(), ToUnixMs: valid.GetFromUnixMs()}},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			store := &fakeBarStore{}
			resp, err := handleListBars(Deps{Bars: store}, tt.req)
			if err != nil {
				t.Fatalf("expected typed validation response, got error: %v", err)
			}
			requireMarketError(t, resp.GetError(), marketErrorInvalidRequest)
			if store.gotID != "" {
				t.Error("store must not be called when validation fails")
			}
		})
	}
}

func TestHandleListBarsUnavailable(t *testing.T) {
	resp, err := handleListBars(Deps{}, validListBarsRequest())
	if err != nil {
		t.Fatalf("expected typed unavailable response, got error: %v", err)
	}
	requireMarketError(t, resp.GetError(), marketErrorUnavailable)
}

func TestHandleListBarsNotFound(t *testing.T) {
	resp, err := handleListBars(Deps{Bars: &fakeBarStore{err: storage.ErrInstrumentNotFound}}, validListBarsRequest())
	if err != nil {
		t.Fatalf("expected typed not_found response, got error: %v", err)
	}
	requireMarketError(t, resp.GetError(), marketErrorNotFound)
}

func TestHandleImportDailyBarsSuccess(t *testing.T) {
	imp := &fakeDailyBarImporter{result: importer.Result{Instruments: 2, Bars: 20}}

	resp, err := handleImportDailyBars(Deps{DailyBarImporter: imp}, validImportRequest())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.GetError() != nil {
		t.Fatalf("unexpected import error: %+v", resp.GetError())
	}
	if !imp.called {
		t.Fatal("expected importer to be called")
	}
	if resp.GetProvider() != "kis" || resp.GetInstrumentCount() != 2 || resp.GetBarCount() != 20 {
		t.Errorf("unexpected response: %+v", resp)
	}
}

func TestHandleImportDailyBarsConvertsRequest(t *testing.T) {
	imp := &fakeDailyBarImporter{}

	if _, err := handleImportDailyBars(Deps{DailyBarImporter: imp}, validImportRequest()); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	got := imp.gotReq
	if got.Provider != market.ProviderKIS {
		t.Errorf("provider mismatch: %q", got.Provider)
	}
	if got.Selector.Kind != market.UniverseSelectorWatchlist {
		t.Errorf("selector kind mismatch: %q", got.Selector.Kind)
	}
	if got.Selector.Market != market.MarketKR || got.Selector.Venue != market.VenueKRX {
		t.Errorf("selector market/venue mismatch: %q/%q", got.Selector.Market, got.Selector.Venue)
	}
	if got.Selector.Name != "my-watchlist" || len(got.Selector.Symbols) != 2 {
		t.Errorf("selector name/symbols mismatch: %+v", got.Selector)
	}
	wantFrom := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
	wantTo := time.Date(2026, 5, 15, 0, 0, 0, 0, time.UTC)
	if !got.From.Equal(wantFrom) || !got.To.Equal(wantTo) {
		t.Errorf("date range mismatch: got %v..%v", got.From, got.To)
	}
}

func validUSImportRequest() *altv1.ImportDailyBarsRequest {
	return &altv1.ImportDailyBarsRequest{
		Provider:     "kis",
		SelectorKind: "watchlist",
		Market:       altv1.Market_MARKET_US,
		Venue:        altv1.Venue_VENUE_NASDAQ,
		Name:         "us-watchlist",
		Symbols:      []string{"AAPL", "SPY"},
		FromYyyymmdd: "20240528",
		ToYyyymmdd:   "20240529",
	}
}

func TestHandleImportDailyBarsConvertsUSRequest(t *testing.T) {
	imp := &fakeDailyBarImporter{result: importer.Result{Instruments: 2, Bars: 4}}

	resp, err := handleImportDailyBars(Deps{DailyBarImporter: imp}, validUSImportRequest())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.GetError() != nil {
		t.Fatalf("unexpected import error: %+v", resp.GetError())
	}
	if !imp.called {
		t.Fatal("expected importer to be called for US watchlist")
	}

	// The US market/venue enums must survive the proto -> domain conversion so
	// the provider can resolve the right overseas exchange.
	got := imp.gotReq
	if got.Provider != market.ProviderKIS {
		t.Errorf("provider mismatch: %q", got.Provider)
	}
	if got.Selector.Market != market.MarketUS || got.Selector.Venue != market.VenueNASDAQ {
		t.Errorf("selector market/venue mismatch: %q/%q, want US/NASDAQ", got.Selector.Market, got.Selector.Venue)
	}
	if len(got.Selector.Symbols) != 2 {
		t.Errorf("selector symbols: got %d, want 2", len(got.Selector.Symbols))
	}
	if resp.GetInstrumentCount() != 2 || resp.GetBarCount() != 4 {
		t.Errorf("unexpected counts: %+v", resp)
	}
}

func TestHandleImportDailyBarsValidation(t *testing.T) {
	// withImport starts from a fully valid request and mutates one field so each
	// case isolates a single validation failure.
	withImport := func(mut func(*altv1.ImportDailyBarsRequest)) *altv1.ImportDailyBarsRequest {
		req := validImportRequest()
		mut(req)
		return req
	}
	tests := []struct {
		name string
		req  *altv1.ImportDailyBarsRequest
	}{
		{"missing provider", withImport(func(r *altv1.ImportDailyBarsRequest) { r.Provider = "" })},
		{"missing selector kind", withImport(func(r *altv1.ImportDailyBarsRequest) { r.SelectorKind = "" })},
		{"missing symbols", withImport(func(r *altv1.ImportDailyBarsRequest) { r.Symbols = nil })},
		{"unsupported market", withImport(func(r *altv1.ImportDailyBarsRequest) { r.Market = altv1.Market(99) })},
		{"unsupported venue", withImport(func(r *altv1.ImportDailyBarsRequest) { r.Venue = altv1.Venue(99) })},
		{"missing from date", withImport(func(r *altv1.ImportDailyBarsRequest) { r.FromYyyymmdd = "" })},
		{"missing to date", withImport(func(r *altv1.ImportDailyBarsRequest) { r.ToYyyymmdd = "" })},
		{"malformed from date", withImport(func(r *altv1.ImportDailyBarsRequest) { r.FromYyyymmdd = "2026-05-01" })},
		{"malformed to date", withImport(func(r *altv1.ImportDailyBarsRequest) { r.ToYyyymmdd = "nope" })},
		{"reversed range", withImport(func(r *altv1.ImportDailyBarsRequest) {
			r.FromYyyymmdd, r.ToYyyymmdd = r.GetToYyyymmdd(), r.GetFromYyyymmdd()
		})},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			imp := &fakeDailyBarImporter{}
			resp, err := handleImportDailyBars(Deps{DailyBarImporter: imp}, tt.req)
			if err != nil {
				t.Fatalf("expected typed validation response, got error: %v", err)
			}
			requireMarketError(t, resp.GetError(), marketErrorInvalidRequest)
			if imp.called {
				t.Error("importer must not be called when validation fails")
			}
		})
	}
}

func TestHandleImportDailyBarsUnavailable(t *testing.T) {
	resp, err := handleImportDailyBars(Deps{}, validImportRequest())
	if err != nil {
		t.Fatalf("expected typed unavailable response, got error: %v", err)
	}
	requireMarketError(t, resp.GetError(), marketErrorUnavailable)
}

func TestHandleImportDailyBarsImporterError(t *testing.T) {
	imp := &fakeDailyBarImporter{err: errors.New("provider boom")}

	resp, err := handleImportDailyBars(Deps{DailyBarImporter: imp}, validImportRequest())
	if err != nil {
		t.Fatalf("expected typed internal response, got error: %v", err)
	}
	requireMarketError(t, resp.GetError(), marketErrorInternal)
}

func TestWorkerImportSocketRoundTrip(t *testing.T) {
	imp := &fakeDailyBarImporter{result: importer.Result{Instruments: 1, Bars: 5}}
	client := startBacktestWorkerTestClient(t, Deps{DailyBarImporter: imp})

	resp, err := protoSocket.SendRequestTyped[*altv1.ImportDailyBarsRequest, *altv1.ImportDailyBarsResponse](
		&client.Communicator,
		validImportRequest(),
		500*time.Millisecond,
	)
	if err != nil {
		t.Fatalf("request should return import response without timeout: %v", err)
	}
	if resp.GetError() != nil {
		t.Fatalf("unexpected import error: %+v", resp.GetError())
	}
	if resp.GetInstrumentCount() != 1 || resp.GetBarCount() != 5 {
		t.Fatalf("unexpected counts: %+v", resp)
	}
	if !imp.called {
		t.Fatal("expected importer to be called through socket")
	}
}

func TestMarketHandlersCoverAllRequests(t *testing.T) {
	registered := make(map[string]bool)
	for _, h := range marketHandlers(Deps{}) {
		if h.requestType == "" {
			t.Error("handler has empty request type")
		}
		if h.register == nil {
			t.Errorf("handler %q has nil register", h.requestType)
		}
		registered[h.requestType] = true
	}

	required := []string{
		"alt.v1.ListInstrumentsRequest",
		"alt.v1.ListBarsRequest",
		"alt.v1.ImportDailyBarsRequest",
	}
	for _, req := range required {
		if !registered[req] {
			t.Errorf("missing handler for %q", req)
		}
	}
}

func TestWorkerMarketSocketValidationReturnsTypedError(t *testing.T) {
	client := startBacktestWorkerTestClient(t, Deps{Bars: &fakeBarStore{}})

	resp, err := protoSocket.SendRequestTyped[*altv1.ListBarsRequest, *altv1.ListBarsResponse](
		&client.Communicator,
		&altv1.ListBarsRequest{},
		500*time.Millisecond,
	)
	if err != nil {
		t.Fatalf("request should return typed error response without timeout: %v", err)
	}
	requireMarketError(t, resp.GetError(), marketErrorInvalidRequest)
}

func TestWorkerMarketSocketListBarsSuccess(t *testing.T) {
	req := validListBarsRequest()
	store := &fakeBarStore{bars: []market.Bar{{
		InstrumentID: "KRX:005930",
		Timeframe:    market.TimeframeDaily,
		Timestamp:    time.UnixMilli(req.GetFromUnixMs()).UTC(),
	}}}
	client := startBacktestWorkerTestClient(t, Deps{Bars: store})

	resp, err := protoSocket.SendRequestTyped[*altv1.ListBarsRequest, *altv1.ListBarsResponse](
		&client.Communicator,
		req,
		500*time.Millisecond,
	)
	if err != nil {
		t.Fatalf("request should return market response without timeout: %v", err)
	}
	if resp.GetError() != nil {
		t.Fatalf("unexpected market error: %+v", resp.GetError())
	}
	if len(resp.GetBars()) != 1 {
		t.Fatalf("expected one bar, got %d", len(resp.GetBars()))
	}
	if store.gotID != "KRX:005930" {
		t.Fatalf("expected request to reach store, got id %q", store.gotID)
	}
}

func requireMarketError(t *testing.T, errInfo *altv1.ErrorInfo, code string) {
	t.Helper()
	if errInfo == nil {
		t.Fatalf("expected ErrorInfo code %q, got nil", code)
	}
	if errInfo.GetCode() != code {
		t.Fatalf("expected ErrorInfo code %q, got %q (%s)", code, errInfo.GetCode(), errInfo.GetMessage())
	}
	if errInfo.GetMessage() == "" {
		t.Fatalf("expected ErrorInfo message for code %q", code)
	}
}
