// Copyright (C) 2025 wangyusong
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

package scraper

import (
	"testing"
	"time"

	. "github.com/onsi/gomega"

	"github.com/glidea/zenfeed/pkg/test"
	timeutil "github.com/glidea/zenfeed/pkg/util/time"
)

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

	// --- Test types ---
	type givenDetail struct {
		config *Config
	}
	type whenDetail struct{} // Validation is the action
	type thenExpected struct {
		expectedConfig *Config // Expected state after validation
		isErr          bool
		wantErrMsg     string
	}

	// --- Test cases ---
	tests := []test.Case[givenDetail, whenDetail, thenExpected]{
		{
			Scenario: "Default values",
			Given:    "a config with zero values for Past and Interval and non-empty Name",
			When:     "validating the config",
			Then:     "should set default Past and Interval, and no error",
			GivenDetail: givenDetail{
				config: &Config{Name: "test"}, // Name is required
			},
			WhenDetail: whenDetail{},
			ThenExpected: thenExpected{
				expectedConfig: &Config{
					Name:     "test",
					Past:     3 * timeutil.Day, // Default Past
					Interval: time.Hour,        // Default/Minimum Interval
				},
				isErr: false,
			},
		},
		{
			Scenario: "Past exceeds maximum",
			Given:    "a config with Past exceeding the maximum limit",
			When:     "validating the config",
			Then:     "should cap Past to the maximum value",
			GivenDetail: givenDetail{
				config: &Config{Name: "test", Past: maxPast + time.Hour},
			},
			WhenDetail: whenDetail{},
			ThenExpected: thenExpected{
				expectedConfig: &Config{
					Name:     "test",
					Past:     maxPast,   // Capped Past
					Interval: time.Hour, // Default Interval
				},
				isErr: false,
			},
		},
		{
			Scenario: "Interval below minimum",
			Given:    "a config with Interval below the minimum limit",
			When:     "validating the config",
			Then:     "should set Interval to the minimum value",
			GivenDetail: givenDetail{
				config: &Config{Name: "test", Interval: time.Second},
			},
			WhenDetail: whenDetail{},
			ThenExpected: thenExpected{
				expectedConfig: &Config{
					Name:     "test",
					Past:     3 * timeutil.Day, // Default Past
					Interval: 10 * time.Minute, // Minimum Interval
				},
				isErr: false,
			},
		},
		{
			Scenario: "Valid values",
			Given:    "a config with valid Past and Interval",
			When:     "validating the config",
			Then:     "should keep the original values",
			GivenDetail: givenDetail{
				config: &Config{
					Name:     "test",
					Past:     24 * time.Hour,
					Interval: 30 * time.Minute,
				},
			},
			WhenDetail: whenDetail{},
			ThenExpected: thenExpected{
				expectedConfig: &Config{
					Name:     "test",
					Past:     24 * time.Hour,
					Interval: 30 * time.Minute,
				},
				isErr: false,
			},
		},
		{
			Scenario: "Missing Name",
			Given:    "a config with an empty Name",
			When:     "validating the config",
			Then:     "should return an error",
			GivenDetail: givenDetail{
				config: &Config{}, // Empty Name
			},
			WhenDetail: whenDetail{},
			ThenExpected: thenExpected{
				isErr:      true,
				wantErrMsg: "name cannot be empty",
			},
		},
	}

	// --- Run tests ---
	for _, tt := range tests {
		t.Run(tt.Scenario, func(t *testing.T) {
			// --- Given ---
			config := tt.GivenDetail.config // Use the config from the test case

			// --- When ---
			err := config.Validate()

			// --- Then ---
			if tt.ThenExpected.isErr {
				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(ContainSubstring(tt.ThenExpected.wantErrMsg))
			} else {
				Expect(err).NotTo(HaveOccurred())
				// Compare the validated config with the expected one
				Expect(config).To(Equal(tt.ThenExpected.expectedConfig))
			}
		})
	}
}

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

	// --- Test types ---
	type givenDetail struct {
		instance     string
		config       *Config
		dependencies Dependencies // Keep dependencies empty for now, focus on config validation
	}
	type whenDetail struct{} // Creation is the action
	type thenExpected struct {
		isErr        bool
		wantErrMsg   string
		validateFunc func(t *testing.T, s Scraper) // Optional validation
	}

	// --- Test cases ---
	validRSSConfig := &ScrapeSourceRSS{URL: "http://valid.com/feed"}
	validBaseConfig := &Config{
		Name:     "test-scraper",
		Interval: 15 * time.Minute, // Valid interval
		RSS:      validRSSConfig,   // Need a valid source for newReader
	}

	tests := []test.Case[givenDetail, whenDetail, thenExpected]{
		{
			Scenario: "Valid Configuration",
			Given:    "a valid config and dependencies",
			When:     "creating a new scraper",
			Then:     "should create scraper successfully",
			GivenDetail: givenDetail{
				instance:     "scraper-1",
				config:       validBaseConfig,
				dependencies: Dependencies{}, // Empty deps are okay for New itself
			},
			WhenDetail: whenDetail{},
			ThenExpected: thenExpected{
				isErr: false,
				validateFunc: func(t *testing.T, s Scraper) {
					Expect(s).NotTo(BeNil())
					Expect(s.Name()).To(Equal("Scraper")) // From Base component
					Expect(s.Instance()).To(Equal("scraper-1"))
					Expect(s.Config()).To(Equal(validBaseConfig)) // Check if config is stored

					// Check internal state if needed (e.g., source type)
					concreteScraper, ok := s.(*scraper)
					Expect(ok).To(BeTrue())
					Expect(concreteScraper.source).NotTo(BeNil())
					_, isRSSReader := concreteScraper.source.(*rssReader)
					Expect(isRSSReader).To(BeTrue())
				},
			},
		},
		{
			Scenario: "Invalid Configuration - Validation Fail",
			Given:    "a config that fails validation (e.g., missing name)",
			When:     "creating a new scraper",
			Then:     "should return a validation error",
			GivenDetail: givenDetail{
				instance: "scraper-invalid",
				config: &Config{ // Missing Name, invalid interval
					Interval: time.Second,
					RSS:      validRSSConfig,
				},
				dependencies: Dependencies{},
			},
			WhenDetail: whenDetail{},
			ThenExpected: thenExpected{
				isErr:      true,
				wantErrMsg: "invalid scraper config: name cannot be empty", // Specific validation error
			},
		},
		{
			Scenario: "Invalid Configuration - Source Creation Fail",
			Given:    "a config that passes validation but has invalid source details",
			When:     "creating a new scraper",
			Then:     "should return an error from source creation",
			GivenDetail: givenDetail{
				instance: "scraper-bad-source",
				config: &Config{
					Name:     "test-bad-source",
					Interval: 15 * time.Minute,
					RSS:      &ScrapeSourceRSS{URL: "invalid-url-format"}, // Invalid RSS URL
				},
				dependencies: Dependencies{},
			},
			WhenDetail: whenDetail{},
			ThenExpected: thenExpected{
				isErr:      true,
				wantErrMsg: "creating source: invalid RSS config: URL must be a valid HTTP/HTTPS URL", // Error from newRSSReader via newReader
			},
		},
		{
			Scenario: "Invalid Configuration - No Source Configured",
			Given:    "a config that passes validation but lacks any source config (RSS is nil)",
			When:     "creating a new scraper",
			Then:     "should return an error indicating unsupported source",
			GivenDetail: givenDetail{
				instance: "scraper-no-source",
				config: &Config{
					Name:     "test-no-source",
					Interval: 15 * time.Minute,
					RSS:      nil, // No source configured
				},
				dependencies: Dependencies{},
			},
			WhenDetail: whenDetail{},
			ThenExpected: thenExpected{
				isErr:      true,
				wantErrMsg: "creating source: source not supported", // Error from newReader
			},
		},
	}

	// --- Run tests ---
	factory := NewFactory() // Use the real factory
	for _, tt := range tests {
		t.Run(tt.Scenario, func(t *testing.T) {
			// --- Given & When ---
			s, err := factory.New(tt.GivenDetail.instance, tt.GivenDetail.config, tt.GivenDetail.dependencies)

			// --- Then ---
			if tt.ThenExpected.isErr {
				Expect(err).To(HaveOccurred())
				// Use MatchError for wrapped errors if necessary, but ContainSubstring is often sufficient
				Expect(err.Error()).To(ContainSubstring(tt.ThenExpected.wantErrMsg))
				Expect(s).To(BeNil())
			} else {
				Expect(err).NotTo(HaveOccurred())
				Expect(s).NotTo(BeNil())
				if tt.ThenExpected.validateFunc != nil {
					tt.ThenExpected.validateFunc(t, s)
				}
			}
		})
	}
}
