/*
 * Copyright (c) 2024 Huawei Technologies Co., Ltd.
 * openFuyao is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *          http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */

package tokenizer

import (
	"context"
	"fmt"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	pb "hermes-router/api/tokenizer/v1"
	"hermes-router/pkg/epp/internal/testutil"

	fwkplugin "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/framework/interface/plugin"
	fwkscheduling "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/framework/interface/scheduling"
)

type initOutcome struct {
	result Backend
	err    error
	block  <-chan struct{}
}

type closeTrackingTokenizer struct {
	*FakeTokenizer
	closed atomic.Bool
}

func (t *closeTrackingTokenizer) Close() error {
	t.closed.Store(true)
	return nil
}

func newTestPlugin(tokenizer Backend, backend string) *Plugin {
	return &Plugin{
		tokenizer: tokenizer,
		backend:   backend,
		state:     initStateReady,
	}
}

func waitForCondition(t *testing.T, timeout time.Duration, check func() bool) {
	t.Helper()
	deadline := time.Now().Add(timeout)
	for time.Now().Before(deadline) {
		if check() {
			return
		}
		time.Sleep(10 * time.Millisecond)
	}
	t.Fatal("expected condition to become true")
}

func newTestPluginWithInitSequence(outcomes ...initOutcome) *Plugin {
	if len(outcomes) == 0 {
		outcomes = []initOutcome{{result: &FakeTokenizer{}}}
	}

	var (
		mu          sync.Mutex
		nextOutcome int
	)

	plugin := &Plugin{
		typedName: fwkplugin.TypedName{Type: PluginType, Name: "test-tokenizer"},
		backend:   defaultBackend,
		model:     "test-model",
		initCfg: GRPCTokenizerConfig{
			SocketPath: "/tmp/test-tokenizer.sock",
			Model:      "test-model",
			Timeout:    defaultTimeout,
		},
		initCtx: context.Background(),
		state:   initStateInitializing,
		sleep: func(context.Context, time.Duration) error {
			return nil
		},
	}

	plugin.newTokenizer = func(ctx context.Context, _ GRPCTokenizerConfig) (Backend, error) {
		mu.Lock()
		index := nextOutcome
		if index < len(outcomes)-1 {
			nextOutcome++
		}
		outcome := outcomes[index]
		mu.Unlock()

		if outcome.block != nil {
			select {
			case <-outcome.block:
			case <-ctx.Done():
				return nil, ctx.Err()
			}
		}

		return outcome.result, outcome.err
	}

	return plugin
}

func TestPrepareRequestData_NilRequest(t *testing.T) {
	plugin := newTestPlugin(&FakeTokenizer{}, "hf")

	if err := plugin.PrepareRequestData(context.Background(), nil, nil); err != nil {
		t.Fatalf("expected nil error, got %v", err)
	}
}

func TestPrepareRequestData_AlreadyTokenized(t *testing.T) {
	existing := &fwkscheduling.TokenizedPrompt{TokenIDs: []uint32{99}}
	plugin := newTestPlugin(&FakeTokenizer{
		TokenizeFn: func(_ context.Context, _ Request) (*TokenizationResult, error) {
			t.Fatal("Tokenize should not be called when request.TokenizedPrompt is already set")
			return nil, nil
		},
	}, "hf")

	req := &fwkscheduling.LLMRequest{
		Body: &fwkscheduling.LLMRequestBody{
			Completions: &fwkscheduling.CompletionsRequest{Prompt: fwkscheduling.Prompt{Raw: "hello"}},
		},
		TokenizedPrompt: existing,
	}

	if err := plugin.PrepareRequestData(context.Background(), req, nil); err != nil {
		t.Fatalf("expected nil error, got %v", err)
	}
	if req.TokenizedPrompt != existing {
		t.Fatal("expected existing tokenized prompt to remain unchanged")
	}
}

func TestPrepareRequestData_CompletionHFUsesTokenize(t *testing.T) {
	plugin := newTestPlugin(&FakeTokenizer{
		TokenizeFn: func(_ context.Context, req Request) (*TokenizationResult, error) {
			if req.Model != "completion-model" {
				t.Fatalf("unexpected model %q", req.Model)
			}
			if req.Prompt != "hello world" {
				t.Fatalf("unexpected prompt %q", req.Prompt)
			}
			return &TokenizationResult{TokenIDs: []uint32{1, 2, 3}}, nil
		},
	}, "hf")

	req := &fwkscheduling.LLMRequest{
		TargetModel: "completion-model",
		Body: &fwkscheduling.LLMRequestBody{
			Completions: &fwkscheduling.CompletionsRequest{Prompt: fwkscheduling.Prompt{Raw: "hello world"}},
		},
	}

	if err := plugin.PrepareRequestData(context.Background(), req, nil); err != nil {
		t.Fatalf("expected nil error, got %v", err)
	}
	if req.TokenizedPrompt == nil {
		t.Fatal("expected tokenized prompt to be populated")
	}
	if got := len(req.TokenizedPrompt.TokenIDs); got != 3 {
		t.Fatalf("expected 3 token ids, got %d", got)
	}
}

func TestPrepareRequestData_CompletionArrayPromptUsesComplete(t *testing.T) {
	plugin := newTestPlugin(&FakeTokenizer{
		CompleteFn: func(_ context.Context, req CompleteRequest) (*TokenizationResult, error) {
			if req.Model != "completion-model" {
				t.Fatalf("unexpected model %q", req.Model)
			}
			if req.PromptText != "hello world" {
				t.Fatalf("unexpected prompt text %q", req.PromptText)
			}
			return &TokenizationResult{TokenIDs: []uint32{10, 20}}, nil
		},
	}, "hf")

	req := &fwkscheduling.LLMRequest{
		TargetModel: "completion-model",
		Body: &fwkscheduling.LLMRequestBody{
			Completions: &fwkscheduling.CompletionsRequest{Prompt: fwkscheduling.Prompt{Strings: []string{"hello", "world"}}},
		},
	}

	if err := plugin.PrepareRequestData(context.Background(), req, nil); err != nil {
		t.Fatalf("expected nil error, got %v", err)
	}
	if req.TokenizedPrompt == nil {
		t.Fatal("expected tokenized prompt to be populated")
	}
	if got := len(req.TokenizedPrompt.TokenIDs); got != 2 {
		t.Fatalf("expected 2 token ids, got %d", got)
	}
}

func TestPrepareRequestData_ChatCompletionUsesChatComplete(t *testing.T) {
	plugin := newTestPlugin(&FakeTokenizer{
		ChatCompleteFn: func(_ context.Context, req ChatCompleteRequest) (*TokenizationResult, error) {
			if req.Model != "chat-model" {
				t.Fatalf("unexpected model %q", req.Model)
			}
			if len(req.Messages) != 2 {
				t.Fatalf("expected 2 messages, got %d", len(req.Messages))
			}
			if len(req.Messages[1].ContentParts) != 2 {
				t.Fatalf("expected multimodal content parts, got %d", len(req.Messages[1].ContentParts))
			}
			return &TokenizationResult{
				TokenIDs: []uint32{5, 6, 7},
				MultiModalFeatures: []fwkscheduling.MultiModalFeature{{
					Modality: fwkscheduling.ModalityImage,
					Hash:     "hash-1",
					Offset:   1,
					Length:   2,
				}},
			}, nil
		},
	}, "vllm")

	req := &fwkscheduling.LLMRequest{
		TargetModel: "chat-model",
		Body: &fwkscheduling.LLMRequestBody{
			ChatCompletions: &fwkscheduling.ChatCompletionsRequest{
				Messages: []fwkscheduling.Message{
					{Role: "system", Content: fwkscheduling.Content{Raw: "be helpful"}},
					{Role: "user", Content: fwkscheduling.Content{Structured: []fwkscheduling.ContentBlock{
						{Type: "text", Text: "describe this"},
						{Type: "image_url", ImageURL: fwkscheduling.ImageBlock{Url: "https://example.test/image.png"}},
					}}},
				},
			},
		},
	}

	if err := plugin.PrepareRequestData(context.Background(), req, nil); err != nil {
		t.Fatalf("expected nil error, got %v", err)
	}
	if req.TokenizedPrompt == nil {
		t.Fatal("expected tokenized prompt to be populated")
	}
	if got := len(req.TokenizedPrompt.MultiModalFeatures); got != 1 {
		t.Fatalf("expected 1 multimodal feature, got %d", got)
	}
}

func TestPrepareRequestDataLogsTokenizationSummary(t *testing.T) {
	ctx, sink := testutil.ContextWithRecordingLogger()
	plugin := newTestPlugin(&FakeTokenizer{
		TokenizeFn: func(_ context.Context, req Request) (*TokenizationResult, error) {
			if req.Model != "summary-model" {
				t.Fatalf("unexpected model %q", req.Model)
			}
			return &TokenizationResult{TokenIDs: []uint32{1, 2, 3, 4}}, nil
		},
	}, "hf")

	req := &fwkscheduling.LLMRequest{
		TargetModel: "summary-model",
		Body: &fwkscheduling.LLMRequestBody{
			Completions: &fwkscheduling.CompletionsRequest{Prompt: fwkscheduling.Prompt{Raw: "hello"}},
		},
	}

	if err := plugin.PrepareRequestData(ctx, req, nil); err != nil {
		t.Fatalf("expected nil error, got %v", err)
	}
	entry := sink.LastInfo("Tokenizer generated tokenized prompt")
	if entry.Message == "" {
		t.Fatal("expected tokenizer success log")
	}
	if entry.Keys["model"] != "summary-model" {
		t.Fatalf("unexpected model: %v", entry.Keys["model"])
	}
	if entry.Keys["tokenCount"] != 4 {
		t.Fatalf("unexpected tokenCount: %v", entry.Keys["tokenCount"])
	}
	if entry.Keys["multimodalFeatureCount"] != 0 {
		t.Fatalf("unexpected multimodalFeatureCount: %v", entry.Keys["multimodalFeatureCount"])
	}
	if _, ok := entry.Keys["durationMs"]; !ok {
		t.Fatal("expected durationMs field")
	}
}

func TestPrepareRequestDataAlreadyTokenizedLogsDebugSkip(t *testing.T) {
	ctx, sink := testutil.ContextWithRecordingLogger()
	plugin := newTestPlugin(&FakeTokenizer{}, "hf")

	req := &fwkscheduling.LLMRequest{
		Body: &fwkscheduling.LLMRequestBody{
			Completions: &fwkscheduling.CompletionsRequest{Prompt: fwkscheduling.Prompt{Raw: "hello"}},
		},
		TokenizedPrompt: &fwkscheduling.TokenizedPrompt{TokenIDs: []uint32{99}},
	}

	if err := plugin.PrepareRequestData(ctx, req, nil); err != nil {
		t.Fatalf("expected nil error, got %v", err)
	}
	entry := sink.LastInfo("Tokenizer skipped tokenization")
	if entry.Message == "" {
		t.Fatal("expected tokenizer skip log")
	}
	if entry.Keys["reason"] != "already_tokenized" {
		t.Fatalf("unexpected skip reason: %v", entry.Keys["reason"])
	}
}

func TestPrepareRequestDataRuntimeFailureLogsAndFailsOpen(t *testing.T) {
	ctx, sink := testutil.ContextWithRecordingLogger()
	plugin := newTestPlugin(&FakeTokenizer{
		TokenizeFn: func(_ context.Context, _ Request) (*TokenizationResult, error) {
			return nil, fmt.Errorf("sidecar unavailable")
		},
	}, "hf")

	req := &fwkscheduling.LLMRequest{
		TargetModel: "runtime-model",
		Body: &fwkscheduling.LLMRequestBody{
			Completions: &fwkscheduling.CompletionsRequest{Prompt: fwkscheduling.Prompt{Raw: "hello"}},
		},
	}

	if err := plugin.PrepareRequestData(ctx, req, nil); err != nil {
		t.Fatalf("expected nil error, got %v", err)
	}
	if req.TokenizedPrompt != nil {
		t.Fatal("expected tokenized prompt to remain nil on runtime error")
	}
	entry := sink.LastError("Tokenizer request tokenization failed; continuing without tokenized prompt")
	if entry.Message == "" {
		t.Fatal("expected runtime fail-open error log")
	}
	if entry.Keys["requestKind"] != "completion" {
		t.Fatalf("unexpected requestKind: %v", entry.Keys["requestKind"])
	}
	if entry.Keys["model"] != "runtime-model" {
		t.Fatalf("unexpected model: %v", entry.Keys["model"])
	}
	if entry.Keys["backend"] != "hf" {
		t.Fatalf("unexpected backend: %v", entry.Keys["backend"])
	}
}

func TestFactoryStartsBackgroundInitWorker(t *testing.T) {
	initializeStarted := make(chan struct{}, 1)
	releaseInitialize := make(chan struct{})
	defer close(releaseInitialize)

	socketPath := startFakeTokenizationServer(t, &fakeTokenizationService{
		initializeFn: func(_ context.Context, req *pb.InitializeRequest) (*pb.InitializeResponse, error) {
			select {
			case initializeStarted <- struct{}{}:
			default:
			}
			<-releaseInitialize
			return &pb.InitializeResponse{
				Model:            req.Model,
				ResolvedProvider: pb.TokenizerProvider_TOKENIZER_PROVIDER_HUGGINGFACE,
			}, nil
		},
	})

	pluginValue, err := Factory(
		"async-tokenizer",
		[]byte(fmt.Sprintf(`{"model":"background-model","socketPath":%q}`, socketPath)),
		fwkplugin.NewEppHandle(context.Background(), nil),
	)
	if err != nil {
		t.Fatalf("expected Factory to start background init worker, got %v", err)
	}
	plugin := pluginValue.(*Plugin)

	select {
	case <-initializeStarted:
	case <-time.After(200 * time.Millisecond):
		t.Fatal("expected background initialization worker to start")
	}

	_, state, _ := plugin.snapshotInitState()
	if state != initStateInitializing {
		t.Fatalf("expected initializing state, got %v", state)
	}
}

func TestPrepareRequestDataWaitsForReadyTokenizerWithoutStartingInit(t *testing.T) {
	releaseInitialize := make(chan struct{})
	plugin := newTestPluginWithInitSequence(initOutcome{
		result: &FakeTokenizer{
			TokenizeFn: func(_ context.Context, req Request) (*TokenizationResult, error) {
				if req.Model != "wait-model" {
					t.Fatalf("unexpected model %q", req.Model)
				}
				if req.Prompt != "hello world" {
					t.Fatalf("unexpected prompt %q", req.Prompt)
				}
				return &TokenizationResult{TokenIDs: []uint32{1, 2, 3}}, nil
			},
		},
		block: releaseInitialize,
	})
	originalNewTokenizer := plugin.newTokenizer
	initStarted := make(chan struct{}, 1)
	unexpectedInitAttempt := make(chan struct{}, 1)
	var (
		initCallsMu sync.Mutex
		initCalls   int
	)
	plugin.newTokenizer = func(ctx context.Context, cfg GRPCTokenizerConfig) (Backend, error) {
		initCallsMu.Lock()
		initCalls++
		callNumber := initCalls
		initCallsMu.Unlock()

		if callNumber == 1 {
			select {
			case initStarted <- struct{}{}:
			default:
			}
		} else {
			select {
			case unexpectedInitAttempt <- struct{}{}:
			default:
			}
		}

		return originalNewTokenizer(ctx, cfg)
	}
	go plugin.runInitLoop()

	select {
	case <-initStarted:
	case <-time.After(time.Second):
		t.Fatal("expected background initialization attempt to start")
	}

	req := &fwkscheduling.LLMRequest{
		TargetModel: "wait-model",
		Body: &fwkscheduling.LLMRequestBody{
			Completions: &fwkscheduling.CompletionsRequest{Prompt: fwkscheduling.Prompt{Raw: "hello world"}},
		},
	}

	_, state, _ := plugin.snapshotInitState()
	if state != initStateInitializing {
		t.Fatalf("expected initializing state, got %v", state)
	}

	done := make(chan error, 1)
	go func() {
		done <- plugin.PrepareRequestData(context.Background(), req, nil)
	}()

	select {
	case err := <-done:
		t.Fatalf("expected request to wait for tokenizer readiness, returned early with %v", err)
	case <-unexpectedInitAttempt:
		t.Fatal("expected PrepareRequestData to wait for readiness without starting another initialization attempt")
	case <-time.After(50 * time.Millisecond):
	}
	close(releaseInitialize)

	select {
	case err := <-done:
		if err != nil {
			t.Fatalf("expected nil error after tokenizer becomes ready, got %v", err)
		}
	case <-time.After(time.Second):
		t.Fatal("expected request to finish once tokenizer becomes ready")
	}

	select {
	case <-unexpectedInitAttempt:
		t.Fatal("expected exactly one tokenizer initialization attempt while waiting for readiness")
	default:
	}

	initCallsMu.Lock()
	gotInitCalls := initCalls
	initCallsMu.Unlock()
	if gotInitCalls != 1 {
		t.Fatalf("expected exactly 1 tokenizer initialization attempt, got %d", gotInitCalls)
	}
	if req.TokenizedPrompt == nil {
		t.Fatal("expected request to use tokenizer once the background worker makes it ready")
	}
}

func TestPrepareRequestDataFailsOpenWithErrorLogWhenTokenizerStaysUnavailable(t *testing.T) {
	ctx, sink := testutil.ContextWithRecordingLogger()
	blockInit := make(chan struct{})
	defer close(blockInit)

	initCtx, cancel := context.WithCancel(context.Background())
	defer cancel()

	plugin := newTestPluginWithInitSequence(initOutcome{
		err:   fmt.Errorf("tokenizer still warming"),
		block: blockInit,
	})
	plugin.initCtx = initCtx
	go plugin.runInitLoop()

	req := &fwkscheduling.LLMRequest{
		TargetModel: "fail-open-model",
		Body: &fwkscheduling.LLMRequestBody{
			Completions: &fwkscheduling.CompletionsRequest{Prompt: fwkscheduling.Prompt{Raw: "hello"}},
		},
	}

	if err := plugin.PrepareRequestData(ctx, req, nil); err != nil {
		t.Fatalf("expected nil error, got %v", err)
	}
	if req.TokenizedPrompt != nil {
		t.Fatal("expected request to fail open while tokenizer is unavailable")
	}
	if sink.LastError("Tokenizer unavailable after bounded readiness wait; continuing without tokenized prompt").Message == "" {
		t.Fatal("expected error log when tokenizer never becomes ready")
	}
}

func TestBackgroundInitWorkerLogsFailureThenReady(t *testing.T) {
	ctx, sink := testutil.ContextWithRecordingLogger()
	readyTokenizer := &FakeTokenizer{}
	plugin := newTestPluginWithInitSequence(
		initOutcome{err: fmt.Errorf("dial tokenizer grpc server: not ready")},
		initOutcome{result: readyTokenizer},
	)
	plugin.initCtx = ctx

	go plugin.runInitLoop()

	waitForCondition(t, time.Second, func() bool {
		tokenizer, state, err := plugin.snapshotInitState()
		return tokenizer == readyTokenizer && state == initStateReady && err == nil
	})

	tokenizer, state, err := plugin.snapshotInitState()
	if tokenizer != readyTokenizer || state != initStateReady || err != nil {
		t.Fatalf("expected ready tokenizer after retries, got tokenizer=%v state=%v err=%v", tokenizer, state, err)
	}
	if sink.LastError("Tokenizer initialization failed; retrying").Message == "" {
		t.Fatal("expected retry error log before tokenizer becomes ready")
	}
	if sink.LastInfo("Tokenizer initialization succeeded").Message == "" {
		t.Fatal("expected ready info log once tokenizer becomes available")
	}
}

func TestBackgroundInitWorkerStopsOnContextCancel(t *testing.T) {
	initCtx, cancel := context.WithCancel(context.Background())
	defer cancel()

	plugin := newTestPluginWithInitSequence(
		initOutcome{err: fmt.Errorf("dial tokenizer grpc server: not ready")},
		initOutcome{result: &FakeTokenizer{}},
	)
	plugin.initCtx = initCtx
	plugin.sleep = func(ctx context.Context, delay time.Duration) error {
		<-ctx.Done()
		return ctx.Err()
	}

	go plugin.runInitLoop()

	waitForCondition(t, time.Second, func() bool {
		_, state, err := plugin.snapshotInitState()
		return state == initStateFailed && err != nil
	})

	cancel()

	waitForCondition(t, 200*time.Millisecond, func() bool {
		tokenizer, state, err := plugin.snapshotInitState()
		return tokenizer == nil && state == initStateFailed && err != nil
	})

	tokenizer, state, err := plugin.snapshotInitState()
	if tokenizer != nil || state != initStateFailed || err == nil {
		t.Fatalf("expected worker to stop after context cancel, got tokenizer=%v state=%v err=%v", tokenizer, state, err)
	}
}

func TestRegisterShutdownCloseClosesTokenizer(t *testing.T) {
	initCtx, cancel := context.WithCancel(context.Background())
	defer cancel()

	closer := &closeTrackingTokenizer{FakeTokenizer: &FakeTokenizer{}}
	plugin := &Plugin{
		initCtx:   initCtx,
		tokenizer: closer,
		state:     initStateReady,
	}

	plugin.registerShutdownClose()
	cancel()

	waitForCondition(t, 200*time.Millisecond, func() bool {
		return closer.closed.Load()
	})
	if !closer.closed.Load() {
		t.Fatal("expected shutdown cleanup to close tokenizer")
	}
}

func TestParseAndValidateParams(t *testing.T) {
	tests := []struct {
		name    string
		raw     []byte
		wantErr string
	}{
		{name: "nil", wantErr: "parameters are required"},
		{name: "missing model", raw: []byte(`{"socketPath":"/tmp/t.sock"}`), wantErr: "model is required"},
		{name: "missing socket", raw: []byte(`{"model":"m"}`), wantErr: "socketPath is required"},
		{name: "bad backend", raw: []byte(`{"model":"m","socketPath":"/tmp/t.sock","backend":"bad"}`), wantErr: "backend must be"},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			_, err := parseAndValidateParams(test.raw)
			if err == nil {
				t.Fatalf("expected error containing %q", test.wantErr)
			}
			if got := err.Error(); got == "" || !strings.Contains(got, test.wantErr) {
				t.Fatalf("expected error containing %q, got %q", test.wantErr, got)
			}
		})
	}

	params, err := parseAndValidateParams([]byte(`{"model":"m","socketPath":"/tmp/t.sock"}`))
	if err != nil {
		t.Fatalf("expected defaults to parse, got %v", err)
	}
	if params.Backend != defaultBackend {
		t.Fatalf("expected default backend %q, got %q", defaultBackend, params.Backend)
	}
}

func TestParseAndValidateParamsTokenizerSource(t *testing.T) {
	params, err := parseAndValidateParams([]byte(`{"model":"glm-51","socketPath":"/tmp/t.sock","tokenizerSource":"/models/glm-base-tok"}`))
	if err != nil {
		t.Fatalf("expected params to parse, got %v", err)
	}
	if params.TokenizerName != "/models/glm-base-tok" {
		t.Fatalf("expected TokenizerName to be populated from the tokenizerSource key, got %q", params.TokenizerName)
	}
}