/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package notification

import (
	"context"
	"os"
	"reflect"
	"runtime"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/apache/answer/internal/schema"
)

func TestParseNewQuestionEmailWorkerQueueSize(t *testing.T) {
	tests := []struct {
		name  string
		value string
		want  int
	}{
		{
			name:  "empty",
			value: "",
			want:  defaultNewQuestionEmailWorkerQueueSize,
		},
		{
			name:  "whitespace",
			value: "   ",
			want:  defaultNewQuestionEmailWorkerQueueSize,
		},
		{
			name:  "invalid",
			value: "invalid",
			want:  defaultNewQuestionEmailWorkerQueueSize,
		},
		{
			name:  "zero",
			value: "0",
			want:  defaultNewQuestionEmailWorkerQueueSize,
		},
		{
			name:  "negative",
			value: "-1",
			want:  defaultNewQuestionEmailWorkerQueueSize,
		},
		{
			name:  "parse int overflow",
			value: "9223372036854775808",
			want:  defaultNewQuestionEmailWorkerQueueSize,
		},
		{
			name:  "positive",
			value: "2048",
			want:  2048,
		},
		{
			name:  "max",
			value: "65536",
			want:  maxNewQuestionEmailWorkerQueueSize,
		},
		{
			name:  "above max",
			value: "65537",
			want:  maxNewQuestionEmailWorkerQueueSize,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := parseNewQuestionEmailWorkerQueueSize(tt.value)
			if got != tt.want {
				t.Fatalf("parseNewQuestionEmailWorkerQueueSize(%q) = %d, want %d", tt.value, got, tt.want)
			}
		})
	}
}

func TestNewQuestionEmailWorkerQueueSizeUnsetEnv(t *testing.T) {
	setNewQuestionEmailWorkerQueueSizeEnv(t, "", false)

	got := newQuestionEmailWorkerQueueSize()
	if got != defaultNewQuestionEmailWorkerQueueSize {
		t.Fatalf("newQuestionEmailWorkerQueueSize() = %d, want %d",
			got, defaultNewQuestionEmailWorkerQueueSize)
	}
}

func TestNewQuestionEmailWorkerWithDefaultsUsesQueueSizeEnv(t *testing.T) {
	tests := []struct {
		name  string
		value string
		want  int
	}{
		{
			name:  "configured",
			value: "2048",
			want:  2048,
		},
		{
			name:  "invalid uses default",
			value: "invalid",
			want:  defaultNewQuestionEmailWorkerQueueSize,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			setNewQuestionEmailWorkerQueueSizeEnv(t, tt.value, true)

			worker := newQuestionEmailWorkerWithDefaults(func() time.Duration { return 0 }, nil)
			defer worker.Close()

			if got := cap(worker.tasks); got != tt.want {
				t.Fatalf("cap(worker.tasks) = %d, want %d", got, tt.want)
			}
		})
	}
}

func TestNewQuestionEmailWorkerDelaysBetweenAttempts(t *testing.T) {
	timerFactory := newFakeNewQuestionEmailTimerFactory()
	sendCh := make(chan newQuestionEmailSendEvent, 2)
	worker := newQuestionEmailWorkerWithBuffer(
		func() time.Duration { return 3 * time.Second },
		newQuestionEmailSendRecorder(sendCh),
		timerFactory.New,
		2,
	)
	defer worker.Close()

	if !worker.TryEnqueue(newQuestionEmailWorkerTask("question-1", "user-1", "user-2")) {
		t.Fatalf("TryEnqueue() = false, want true")
	}

	first := receiveNewQuestionEmailSend(t, sendCh)
	if first.userID != "user-1" {
		t.Fatalf("first send user = %s, want user-1", first.userID)
	}
	timer := timerFactory.WaitForTimer(t)
	assertNoNewQuestionEmailSend(t, sendCh)
	timer.Fire()

	second := receiveNewQuestionEmailSend(t, sendCh)
	if second.userID != "user-2" {
		t.Fatalf("second send user = %s, want user-2", second.userID)
	}
	assertUniqueNewQuestionUnsubscribeCodes(t, []string{
		first.rawData.UnsubscribeCode,
		second.rawData.UnsubscribeCode,
	})
	if got := timerFactory.Durations(); !reflect.DeepEqual(got, []time.Duration{3 * time.Second}) {
		t.Fatalf("timer durations = %v, want [3s]", got)
	}
}

func TestNewQuestionEmailWorkerDelayContinuesAcrossTaskBoundaries(t *testing.T) {
	timerFactory := newFakeNewQuestionEmailTimerFactory()
	sendCh := make(chan newQuestionEmailSendEvent, 2)
	worker := newQuestionEmailWorkerWithBuffer(
		func() time.Duration { return 5 * time.Second },
		newQuestionEmailSendRecorder(sendCh),
		timerFactory.New,
		2,
	)
	defer worker.Close()

	if !worker.TryEnqueue(newQuestionEmailWorkerTask("question-1", "user-1")) {
		t.Fatalf("TryEnqueue() task 1 = false, want true")
	}
	first := receiveNewQuestionEmailSend(t, sendCh)
	if first.userID != "user-1" {
		t.Fatalf("first send user = %s, want user-1", first.userID)
	}

	if !worker.TryEnqueue(newQuestionEmailWorkerTask("question-2", "user-2")) {
		t.Fatalf("TryEnqueue() task 2 = false, want true")
	}
	timer := timerFactory.WaitForTimer(t)
	assertNoNewQuestionEmailSend(t, sendCh)
	timer.Fire()

	second := receiveNewQuestionEmailSend(t, sendCh)
	if second.userID != "user-2" {
		t.Fatalf("second send user = %s, want user-2", second.userID)
	}
}

func TestNewQuestionEmailWorkerZeroIntervalSendsWithoutTimers(t *testing.T) {
	var timerCount int
	sendCh := make(chan newQuestionEmailSendEvent, 3)
	worker := newQuestionEmailWorkerWithBuffer(
		func() time.Duration { return 0 },
		newQuestionEmailSendRecorder(sendCh),
		func(time.Duration) newQuestionEmailTimer {
			timerCount++
			return newFakeNewQuestionEmailTimer()
		},
		2,
	)
	defer worker.Close()

	if !worker.TryEnqueue(newQuestionEmailWorkerTask("question-1", "user-1", "user-2", "user-3")) {
		t.Fatalf("TryEnqueue() = false, want true")
	}

	gotUsers := []string{
		receiveNewQuestionEmailSend(t, sendCh).userID,
		receiveNewQuestionEmailSend(t, sendCh).userID,
		receiveNewQuestionEmailSend(t, sendCh).userID,
	}
	if !reflect.DeepEqual(gotUsers, []string{"user-1", "user-2", "user-3"}) {
		t.Fatalf("send users = %v, want [user-1 user-2 user-3]", gotUsers)
	}
	if timerCount != 0 {
		t.Fatalf("timer count = %d, want 0", timerCount)
	}
}

func TestNewQuestionEmailWorkerCloseCancelsPendingWaitAndDropsQueuedTasks(t *testing.T) {
	timerFactory := newFakeNewQuestionEmailTimerFactory()
	sendCh := make(chan newQuestionEmailSendEvent, 3)
	worker := newQuestionEmailWorkerWithBuffer(
		func() time.Duration { return time.Hour },
		newQuestionEmailSendRecorder(sendCh),
		timerFactory.New,
		2,
	)

	if !worker.TryEnqueue(newQuestionEmailWorkerTask("question-1", "user-1", "user-2")) {
		t.Fatalf("TryEnqueue() task 1 = false, want true")
	}
	if !worker.TryEnqueue(newQuestionEmailWorkerTask("question-2", "user-3")) {
		t.Fatalf("TryEnqueue() task 2 = false, want true")
	}

	first := receiveNewQuestionEmailSend(t, sendCh)
	if first.userID != "user-1" {
		t.Fatalf("first send user = %s, want user-1", first.userID)
	}
	timer := timerFactory.WaitForTimer(t)

	worker.Close()
	timer.AssertStopped(t)
	assertNoNewQuestionEmailSend(t, sendCh)
	if got := len(worker.tasks); got != 0 {
		t.Fatalf("pending tasks after Close() = %d, want 0", got)
	}
	if worker.TryEnqueue(newQuestionEmailWorkerTask("question-3", "user-4")) {
		t.Fatalf("TryEnqueue() after Close() = true, want false")
	}
}

func TestNewQuestionEmailWorkerProcessesSerially(t *testing.T) {
	entered := make(chan string, 2)
	releaseFirst := make(chan struct{})
	worker := newQuestionEmailWorkerWithBuffer(
		func() time.Duration { return 0 },
		func(_ context.Context, userID string, _ *schema.NewQuestionTemplateRawData) {
			entered <- userID
			if userID == "user-1" {
				<-releaseFirst
			}
		},
		nil,
		2,
	)
	defer worker.Close()

	if !worker.TryEnqueue(newQuestionEmailWorkerTask("question-1", "user-1", "user-2")) {
		t.Fatalf("TryEnqueue() = false, want true")
	}
	if got := receiveString(t, entered); got != "user-1" {
		t.Fatalf("first send user = %s, want user-1", got)
	}
	assertNoString(t, entered)

	close(releaseFirst)
	if got := receiveString(t, entered); got != "user-2" {
		t.Fatalf("second send user = %s, want user-2", got)
	}
}

func TestNewQuestionEmailWorkerBuildsFreshRawDataPerAttempt(t *testing.T) {
	sendCh := make(chan newQuestionEmailSendEvent, 2)
	worker := newQuestionEmailWorkerWithBuffer(
		func() time.Duration { return 0 },
		func(_ context.Context, userID string, rawData *schema.NewQuestionTemplateRawData) {
			if rawData.QuestionAuthorUserID != "" {
				t.Errorf("QuestionAuthorUserID = %q, want empty", rawData.QuestionAuthorUserID)
			}
			if userID == "user-1" {
				rawData.Tags[0] = "mutated"
				rawData.TagIDs[0] = "mutated"
			}
			sendCh <- newQuestionEmailSendEvent{userID: userID, rawData: rawData}
		},
		nil,
		2,
	)
	defer worker.Close()

	if !worker.TryEnqueue(newQuestionEmailTask{
		UserIDs:       []string{"user-1", "user-2"},
		QuestionTitle: "Question",
		QuestionID:    "question-1",
		Tags:          []string{"go"},
		TagIDs:        []string{"tag-1"},
	}) {
		t.Fatalf("TryEnqueue() = false, want true")
	}

	first := receiveNewQuestionEmailSend(t, sendCh)
	second := receiveNewQuestionEmailSend(t, sendCh)
	if first.rawData.UnsubscribeCode == "" || second.rawData.UnsubscribeCode == "" {
		t.Fatalf("unsubscribe codes must be non-empty: %q %q",
			first.rawData.UnsubscribeCode, second.rawData.UnsubscribeCode)
	}
	if first.rawData.UnsubscribeCode == second.rawData.UnsubscribeCode {
		t.Fatalf("unsubscribe codes must be unique, both were %q", first.rawData.UnsubscribeCode)
	}
	if !reflect.DeepEqual(second.rawData.Tags, []string{"go"}) ||
		!reflect.DeepEqual(second.rawData.TagIDs, []string{"tag-1"}) {
		t.Fatalf("second raw data tags = %v/%v, want original values",
			second.rawData.Tags, second.rawData.TagIDs)
	}
}

func TestNewQuestionEmailWorkerTryEnqueueCopiesTaskAndFailsFast(t *testing.T) {
	worker := newUnstartedNewQuestionEmailWorkerForTest()
	task := newQuestionEmailTask{
		UserIDs:       []string{"user-1"},
		QuestionTitle: "Question",
		QuestionID:    "question-1",
		Tags:          []string{"go"},
		TagIDs:        []string{"tag-1"},
	}
	if !worker.TryEnqueue(task) {
		t.Fatalf("TryEnqueue() = false, want true")
	}
	task.UserIDs[0] = "mutated-user"
	task.Tags[0] = "mutated-tag"
	task.TagIDs[0] = "mutated-tag-id"

	queuedTask := <-worker.tasks
	if !reflect.DeepEqual(queuedTask.UserIDs, []string{"user-1"}) ||
		!reflect.DeepEqual(queuedTask.Tags, []string{"go"}) ||
		!reflect.DeepEqual(queuedTask.TagIDs, []string{"tag-1"}) {
		t.Fatalf("queued task was mutated: %+v", queuedTask)
	}

	if !worker.TryEnqueue(newQuestionEmailWorkerTask("question-2", "user-2")) {
		t.Fatalf("TryEnqueue() refill = false, want true")
	}
	if worker.TryEnqueue(newQuestionEmailWorkerTask("question-3", "user-3")) {
		t.Fatalf("TryEnqueue() with full queue = true, want false")
	}

	worker.Close()
	if worker.TryEnqueue(newQuestionEmailWorkerTask("question-4", "user-4")) {
		t.Fatalf("TryEnqueue() after Close() = true, want false")
	}

	canceledWorker := newUnstartedNewQuestionEmailWorkerForTest()
	canceledWorker.cancel()
	if canceledWorker.TryEnqueue(newQuestionEmailWorkerTask("question-5", "user-5")) {
		t.Fatalf("TryEnqueue() after cancel = true, want false")
	}
}

func TestNewQuestionEmailWorkerTryEnqueueConcurrentClose(t *testing.T) {
	const (
		iterations = 100
		senders    = 32
	)

	for iteration := range iterations {
		worker := newUnstartedNewQuestionEmailWorkerForTest()
		if !worker.TryEnqueue(newQuestionEmailWorkerTask("already-queued", "queued-user")) {
			t.Fatalf("iteration %d: pre-fill TryEnqueue() = false, want true", iteration)
		}

		start := make(chan struct{})
		ready := make(chan struct{}, senders)
		panicCh := make(chan any, senders)
		var closeObserved atomic.Bool
		var acceptedAfterCloseObserved atomic.Int64
		var wg sync.WaitGroup

		for range senders {
			//nolint:modernize // CI uses Go 1.23, which does not support WaitGroup.Go.
			wg.Add(1)
			go func() {
				defer wg.Done()
				defer func() {
					if recovered := recover(); recovered != nil {
						panicCh <- recovered
					}
				}()

				ready <- struct{}{}
				<-start
				for {
					accepted := worker.TryEnqueue(newQuestionEmailWorkerTask("question", "user"))
					if accepted && closeObserved.Load() {
						acceptedAfterCloseObserved.Add(1)
					}
					if closeObserved.Load() {
						return
					}
					runtime.Gosched()
				}
			}()
		}
		for range senders {
			<-ready
		}

		closeDoneObserved := make(chan struct{})
		go func() {
			<-worker.ctx.Done()
			closeObserved.Store(true)
			close(closeDoneObserved)
		}()

		close(start)
		runtime.Gosched()

		closeDone := make(chan struct{})
		go func() {
			worker.Close()
			close(closeDone)
		}()

		select {
		case <-closeDone:
		case <-time.After(time.Second):
			t.Fatalf("iteration %d: Close() did not return", iteration)
		}
		select {
		case <-closeDoneObserved:
		case <-time.After(time.Second):
			t.Fatalf("iteration %d: close was not observed", iteration)
		}

		wgDone := make(chan struct{})
		go func() {
			wg.Wait()
			close(wgDone)
		}()
		select {
		case <-wgDone:
		case <-time.After(time.Second):
			t.Fatalf("iteration %d: TryEnqueue goroutines did not return", iteration)
		}

		select {
		case recovered := <-panicCh:
			t.Fatalf("iteration %d: TryEnqueue panicked during Close(): %v", iteration, recovered)
		default:
		}
		if got := acceptedAfterCloseObserved.Load(); got != 0 {
			t.Fatalf("iteration %d: accepted %d enqueue attempts after close was observed, want 0",
				iteration, got)
		}
		if worker.TryEnqueue(newQuestionEmailWorkerTask("after-close", "user")) {
			t.Fatalf("iteration %d: TryEnqueue() after Close() = true, want false", iteration)
		}
		if got := len(worker.tasks); got != 0 {
			t.Fatalf("iteration %d: pending tasks after Close() = %d, want 0", iteration, got)
		}
	}
}

func TestWaitNewQuestionEmailIntervalCancel(t *testing.T) {
	timerFactory := newFakeNewQuestionEmailTimerFactory()
	ctx, cancel := context.WithCancel(context.Background())
	done := make(chan bool, 1)
	go func() {
		done <- waitNewQuestionEmailInterval(ctx, time.Minute, timerFactory.New)
	}()

	timer := timerFactory.WaitForTimer(t)
	cancel()

	select {
	case got := <-done:
		if got {
			t.Fatalf("waitNewQuestionEmailInterval() = true, want false")
		}
	case <-time.After(time.Second):
		t.Fatalf("waitNewQuestionEmailInterval() did not return after cancellation")
	}
	timer.AssertStopped(t)
}

type newQuestionEmailSendEvent struct {
	userID  string
	rawData *schema.NewQuestionTemplateRawData
}

func newQuestionEmailSendRecorder(sendCh chan<- newQuestionEmailSendEvent) newQuestionNotificationEmailSender {
	return func(_ context.Context, userID string, rawData *schema.NewQuestionTemplateRawData) {
		sendCh <- newQuestionEmailSendEvent{userID: userID, rawData: rawData}
	}
}

func newQuestionEmailWorkerTask(questionID string, userIDs ...string) newQuestionEmailTask {
	return newQuestionEmailTask{
		UserIDs:       userIDs,
		QuestionTitle: "Question",
		QuestionID:    questionID,
		Tags:          []string{"go"},
		TagIDs:        []string{"tag-1"},
	}
}

func newUnstartedNewQuestionEmailWorkerForTest() *newQuestionEmailWorker {
	ctx, cancel := context.WithCancel(context.Background())
	return &newQuestionEmailWorker{
		tasks:        make(chan newQuestionEmailTask, 1),
		interval:     func() time.Duration { return 0 },
		timerFactory: newRealNewQuestionEmailTimer,
		ctx:          ctx,
		cancel:       cancel,
	}
}

func setNewQuestionEmailWorkerQueueSizeEnv(t *testing.T, value string, set bool) {
	t.Helper()

	oldValue, oldSet := os.LookupEnv(newQuestionEmailWorkerQueueSizeEnv)
	if set {
		if err := os.Setenv(newQuestionEmailWorkerQueueSizeEnv, value); err != nil {
			t.Fatalf("set env: %v", err)
		}
	} else {
		if err := os.Unsetenv(newQuestionEmailWorkerQueueSizeEnv); err != nil {
			t.Fatalf("unset env: %v", err)
		}
	}
	t.Cleanup(func() {
		if oldSet {
			_ = os.Setenv(newQuestionEmailWorkerQueueSizeEnv, oldValue)
		} else {
			_ = os.Unsetenv(newQuestionEmailWorkerQueueSizeEnv)
		}
	})
}

func receiveNewQuestionEmailSend(t *testing.T, sendCh <-chan newQuestionEmailSendEvent) newQuestionEmailSendEvent {
	t.Helper()
	select {
	case event := <-sendCh:
		return event
	case <-time.After(time.Second):
		t.Fatalf("timed out waiting for new question email send")
		return newQuestionEmailSendEvent{}
	}
}

func assertNoNewQuestionEmailSend(t *testing.T, sendCh <-chan newQuestionEmailSendEvent) {
	t.Helper()
	select {
	case event := <-sendCh:
		t.Fatalf("unexpected new question email send: %+v", event)
	default:
	}
}

func receiveString(t *testing.T, ch <-chan string) string {
	t.Helper()
	select {
	case value := <-ch:
		return value
	case <-time.After(time.Second):
		t.Fatalf("timed out waiting for string")
		return ""
	}
}

func assertNoString(t *testing.T, ch <-chan string) {
	t.Helper()
	select {
	case value := <-ch:
		t.Fatalf("unexpected string: %s", value)
	default:
	}
}

type fakeNewQuestionEmailTimerFactory struct {
	timers    chan *fakeNewQuestionEmailTimer
	mu        sync.Mutex
	durations []time.Duration
}

func newFakeNewQuestionEmailTimerFactory() *fakeNewQuestionEmailTimerFactory {
	return &fakeNewQuestionEmailTimerFactory{
		timers: make(chan *fakeNewQuestionEmailTimer, 16),
	}
}

func (f *fakeNewQuestionEmailTimerFactory) New(duration time.Duration) newQuestionEmailTimer {
	timer := newFakeNewQuestionEmailTimer()

	f.mu.Lock()
	f.durations = append(f.durations, duration)
	f.mu.Unlock()

	f.timers <- timer
	return timer
}

func (f *fakeNewQuestionEmailTimerFactory) WaitForTimer(t *testing.T) *fakeNewQuestionEmailTimer {
	t.Helper()
	select {
	case timer := <-f.timers:
		return timer
	case <-time.After(time.Second):
		t.Fatalf("timed out waiting for timer")
		return nil
	}
}

func (f *fakeNewQuestionEmailTimerFactory) Durations() []time.Duration {
	f.mu.Lock()
	defer f.mu.Unlock()

	durations := make([]time.Duration, len(f.durations))
	copy(durations, f.durations)
	return durations
}

type fakeNewQuestionEmailTimer struct {
	ch      chan time.Time
	stopped chan struct{}
	once    sync.Once
}

func newFakeNewQuestionEmailTimer() *fakeNewQuestionEmailTimer {
	return &fakeNewQuestionEmailTimer{
		ch:      make(chan time.Time, 1),
		stopped: make(chan struct{}),
	}
}

func (t *fakeNewQuestionEmailTimer) C() <-chan time.Time {
	return t.ch
}

func (t *fakeNewQuestionEmailTimer) Stop() {
	t.once.Do(func() {
		close(t.stopped)
	})
}

func (t *fakeNewQuestionEmailTimer) Fire() {
	t.ch <- time.Now()
}

func (t *fakeNewQuestionEmailTimer) AssertStopped(tb testing.TB) {
	tb.Helper()
	select {
	case <-t.stopped:
	case <-time.After(time.Second):
		tb.Fatalf("timer was not stopped")
	}
}
