package rule

import (
	"context"
	"testing"
	"time"

	. "github.com/onsi/gomega"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/mock"

	"github.com/glidea/zenfeed/pkg/component"
	"github.com/glidea/zenfeed/pkg/model"
	"github.com/glidea/zenfeed/pkg/storage/feed"
	"github.com/glidea/zenfeed/pkg/storage/feed/block"
	"github.com/glidea/zenfeed/pkg/test"
)

func TestWatchExecute(t *testing.T) {
	RegisterTestingT(t)

	// --- Test types ---
	type givenDetail struct {
		config          *Config
		feedStorageMock func(m *mock.Mock) // Function to set expectations
	}
	type whenDetail struct {
		start time.Time
		end   time.Time
	}
	type thenExpected struct {
		queryCalled bool
		queryOpts   *block.QueryOptions   // Expected query options
		sentToOut   map[time.Time]*Result // Expected results sent to Out, keyed by interval start time
		err         error                 // Expected error (can be wrapped)
		isErr       bool
	}

	// --- Test cases ---
	watchInterval := 10 * time.Minute
	baseConfig := &Config{
		Name:          "test-watch",
		WatchInterval: watchInterval,
		Threshold:     0.7,
		Query:         "test query",
		LabelFilters:  []string{"source:test"},
	}
	now := time.Date(2024, 1, 15, 10, 35, 0, 0, time.Local) // Example time: 10:35
	// The execute function calculates start/end based on its input 'end' time and interval
	// Let's define the input range for the 'execute' call
	execEnd := now
	execStart := execEnd.Add(-3 * watchInterval) // Matches the logic in watch.go iter()

	// Define feed times relative to the interval
	interval1Start := time.Unix(now.Unix(), 0).Truncate(watchInterval) // 10:30
	interval2Start := interval1Start.Add(-watchInterval)               // 10:20
	// interval3Start := interval2Start.Add(-watchInterval)                // 10:10, covered by execStart

	feedTime1 := interval1Start.Add(1 * time.Minute) // 10:31 (belongs to 10:30 interval)
	feedTime2 := interval2Start.Add(2 * time.Minute) // 10:22 (belongs to 10:20 interval)
	feedTime3 := interval2Start.Add(5 * time.Minute) // 10:25 (belongs to 10:20 interval)

	mockFeeds := []*block.FeedVO{
		{Feed: &model.Feed{ID: 1, Time: feedTime1, Labels: model.Labels{{Key: "content_hash", Value: "a"}}}},
		{Feed: &model.Feed{ID: 2, Time: feedTime2, Labels: model.Labels{{Key: "content_hash", Value: "b"}}}},
		{Feed: &model.Feed{ID: 3, Time: feedTime3, Labels: model.Labels{{Key: "content_hash", Value: "c"}}}},
	}
	queryError := errors.New("database error")

	tests := []test.Case[givenDetail, whenDetail, thenExpected]{
		{
			Scenario: "Feeds found, should query and notify grouped by interval",
			Given:    "a watch config and FeedStorage returns feeds across intervals",
			When:     "execute is called with a time range",
			Then:     "FeedStorage should be queried, and results grouped by WatchInterval sent to Out",
			GivenDetail: givenDetail{
				config: baseConfig,
				feedStorageMock: func(m *mock.Mock) {
					m.On("Query", mock.Anything, mock.AnythingOfType("block.QueryOptions")).
						Return(mockFeeds, nil)
				},
			},
			WhenDetail: whenDetail{
				start: execStart,
				end:   execEnd,
			},
			ThenExpected: thenExpected{
				queryCalled: true,
				queryOpts: &block.QueryOptions{
					Query:        baseConfig.Query,
					Threshold:    baseConfig.Threshold,
					LabelFilters: baseConfig.LabelFilters,
					Start:        execStart,
					End:          execEnd,
					Limit:        500,
				},
				sentToOut: map[time.Time]*Result{
					interval1Start: { // 10:30 interval
						Rule: baseConfig.Name,
						Time: interval1Start,
						Feeds: []*block.FeedVO{
							mockFeeds[0], // ID 1 at 10:31
						},
					},
					interval2Start: { // 10:20 interval
						Rule: baseConfig.Name,
						Time: interval2Start,
						Feeds: []*block.FeedVO{
							mockFeeds[1], // ID 2 at 10:22
							mockFeeds[2], // ID 3 at 10:25
						},
					},
				},
			},
		},
		{
			Scenario: "No feeds found, should query but not notify",
			Given:    "a watch config and FeedStorage returns no feeds",
			When:     "execute is called",
			Then:     "FeedStorage should be queried but nothing sent to Out",
			GivenDetail: givenDetail{
				config: baseConfig,
				feedStorageMock: func(m *mock.Mock) {
					m.On("Query", mock.Anything, mock.AnythingOfType("block.QueryOptions")).
						Return([]*block.FeedVO{}, nil) // Empty result
				},
			},
			WhenDetail: whenDetail{
				start: execStart,
				end:   execEnd,
			},
			ThenExpected: thenExpected{
				queryCalled: true,
				queryOpts: &block.QueryOptions{
					Query:        baseConfig.Query,
					Threshold:    baseConfig.Threshold,
					LabelFilters: baseConfig.LabelFilters,
					Start:        execStart,
					End:          execEnd,
					Limit:        500,
				},
				sentToOut: map[time.Time]*Result{}, // Expect empty map or nil
			},
		},
		{
			Scenario: "FeedStorage query error, should return error",
			Given:    "a watch config and FeedStorage returns an error",
			When:     "execute is called",
			Then:     "FeedStorage should be queried and an error returned",
			GivenDetail: givenDetail{
				config: baseConfig,
				feedStorageMock: func(m *mock.Mock) {
					m.On("Query", mock.Anything, mock.AnythingOfType("block.QueryOptions")).
						Return([]*block.FeedVO{}, queryError)
				},
			},
			WhenDetail: whenDetail{
				start: execStart,
				end:   execEnd,
			},
			ThenExpected: thenExpected{
				queryCalled: true,
				queryOpts: &block.QueryOptions{ // Still expect query options to be set
					Query:        baseConfig.Query,
					Threshold:    baseConfig.Threshold,
					LabelFilters: baseConfig.LabelFilters,
					Start:        execStart,
					End:          execEnd,
					Limit:        500,
				},
				sentToOut: nil, // Nothing sent on error
				err:       errors.Wrap(queryError, "query"),
				isErr:     true,
			},
		},
	}

	// --- Run tests ---
	for _, tt := range tests {
		t.Run(tt.Scenario, func(t *testing.T) {
			// --- Given ---
			configCopy := *tt.GivenDetail.config // Use a copy for safety

			outCh := make(chan *Result, 5) // Buffer size accommodates potential multiple sends
			var capturedOpts block.QueryOptions
			var mockStorageInstance *mock.Mock

			// Create mock factory using feed.NewFactory and capture the mock instance
			mockOption := component.MockOption(func(m *mock.Mock) {
				mockStorageInstance = m // Capture the mock instance
				// Setup mock expectation for FeedStorage.Query, including option capture
				if tt.GivenDetail.feedStorageMock != nil {
					tt.GivenDetail.feedStorageMock(m)
					// Enhance the mock setup to capture arguments
					for _, call := range m.ExpectedCalls {
						if call.Method == "Query" {
							for i, arg := range call.Arguments {
								if _, ok := arg.(mock.AnythingOfTypeArgument); ok && i == 1 { // Assuming options is the second argument (index 1)
									call.Arguments[i] = mock.MatchedBy(func(opts block.QueryOptions) bool {
										capturedOpts = opts // Capture the options
										return true
									})
									break
								}
							}
							break // Assume only one Query expectation per test case here
						}
					}
				}
			})
			// NOTE: feed.NewFactory needs *config.App, we pass nil as it's not used by the mock
			mockFeedFactory := feed.NewFactory(mockOption)
			mockFeedStorage, factoryErr := mockFeedFactory.New(component.Global, nil, feed.Dependencies{}) // Use factory to create mock
			Expect(factoryErr).NotTo(HaveOccurred())

			dependencies := Dependencies{
				FeedStorage: mockFeedStorage, // Use the created mock storage
				Out:         outCh,
			}

			// Use the specific type `watch` for testing its method
			r := &watch{
				Base: component.New(&component.BaseConfig[Config, Dependencies]{
					Name:         "WatchRuler",
					Instance:     "test-instance",
					Config:       &configCopy,
					Dependencies: dependencies,
				}),
			}

			// --- When ---
			err := r.execute(context.Background(), tt.WhenDetail.start, tt.WhenDetail.end)

			// --- Then ---
			close(outCh) // Close channel to range over received results

			if tt.ThenExpected.isErr {
				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(ContainSubstring(tt.ThenExpected.err.Error())) // Check if the error contains the expected wrapped message
				Expect(len(outCh)).To(Equal(0))                                       // No results sent on error
			} else {
				Expect(err).NotTo(HaveOccurred())

				receivedResults := make(map[time.Time]*Result)
				for res := range outCh {
					receivedResults[res.Time] = res
				}

				Expect(len(receivedResults)).To(Equal(len(tt.ThenExpected.sentToOut)), "Mismatch in number of results sent")
				for expectedTime, expectedResult := range tt.ThenExpected.sentToOut {
					receivedResult, ok := receivedResults[expectedTime]
					Expect(ok).To(BeTrue(), "Expected result for time %v not found", expectedTime)
					Expect(receivedResult.Rule).To(Equal(expectedResult.Rule))
					Expect(receivedResult.Time.Unix()).To(Equal(expectedResult.Time.Unix()))
					Expect(receivedResult.Feeds).To(ConsistOf(expectedResult.Feeds)) // Use ConsistOf for order-independent comparison
				}
			}

			// Verify FeedStorage.Query call and options using the captured mock instance
			if mockStorageInstance != nil {
				if tt.ThenExpected.queryCalled {
					mockStorageInstance.AssertCalled(t, "Query", mock.Anything, mock.AnythingOfType("block.QueryOptions"))
					// Assert specific fields of the captured options
					Expect(capturedOpts.Query).To(Equal(tt.ThenExpected.queryOpts.Query), "Query string mismatch")
					Expect(capturedOpts.Threshold).To(Equal(tt.ThenExpected.queryOpts.Threshold), "Threshold mismatch")
					Expect(capturedOpts.LabelFilters).To(Equal(tt.ThenExpected.queryOpts.LabelFilters), "LabelFilters mismatch")
					Expect(capturedOpts.Start.Unix()).To(Equal(tt.ThenExpected.queryOpts.Start.Unix()), "Start time mismatch")
					Expect(capturedOpts.End.Unix()).To(Equal(tt.ThenExpected.queryOpts.End.Unix()), "End time mismatch")
					Expect(capturedOpts.Limit).To(Equal(tt.ThenExpected.queryOpts.Limit), "Limit mismatch")
				} else {
					mockStorageInstance.AssertNotCalled(t, "Query", mock.Anything, mock.Anything)
				}
				// mockStorageInstance.AssertExpectations(t) // Uncomment for strict expectation matching if needed
			} else if tt.ThenExpected.queryCalled {
				t.Fatal("Expected query call but mock instance was not captured")
			}
		})
	}
}
