package discord

import (
	"errors"
	"io"
	"net/http"
	"net/url"
	"strings"

	"github.com/onsi/ginkgo/v2"
	"github.com/onsi/gomega"
	"github.com/stretchr/testify/mock"

	"github.com/nicholas-fedor/shoutrrr/pkg/format"
	"github.com/nicholas-fedor/shoutrrr/pkg/services/chat/discord/mocks"
	"github.com/nicholas-fedor/shoutrrr/pkg/types"
	mockTypes "github.com/nicholas-fedor/shoutrrr/pkg/types/mocks"
)

const testWebhookURLAlt = "https://discord.com/api/webhooks/123/abc"

var _ = ginkgo.Describe("Discord Service Unit Tests", func() {
	ginkgo.Describe("Service.Send method", func() {
		var service *Service

		ginkgo.BeforeEach(func() {
			service = &Service{
				Config: &Config{
					WebhookID: "123456789",
					Token:     "test-token",
				},
			}
			service.SetLogger(&mockTypes.MockStdLogger{})
		})

		ginkgo.It("should send plain text message successfully", func() {
			mockClient := mocks.NewMockHTTPClient(ginkgo.GinkgoT())
			service.HTTPClient = mockClient
			mockClient.On("Do", mock.Anything).Return(&http.Response{
				StatusCode: http.StatusNoContent,
				Body:       io.NopCloser(strings.NewReader("")),
			}, nil)

			message := "Hello, Discord!"
			params := &types.Params{}

			err := service.Send(message, params)

			gomega.Expect(err).ToNot(gomega.HaveOccurred())
			mockClient.AssertNumberOfCalls(ginkgo.GinkgoT(), "Do", 1)
		})

		ginkgo.It("should send JSON message in JSON mode", func() {
			mockClient := mocks.NewMockHTTPClient(ginkgo.GinkgoT())
			service.HTTPClient = mockClient
			service.Config.JSON = true

			mockClient.On("Do", mock.Anything).Return(&http.Response{
				StatusCode: http.StatusNoContent,
				Body:       io.NopCloser(strings.NewReader("")),
			}, nil)

			jsonMessage := `{"content":"JSON message"}`
			params := &types.Params{}

			err := service.Send(jsonMessage, params)

			gomega.Expect(err).ToNot(gomega.HaveOccurred())
			mockClient.AssertNumberOfCalls(ginkgo.GinkgoT(), "Do", 1)
		})

		ginkgo.It("should handle empty message", func() {
			message := ""
			params := &types.Params{}

			err := service.Send(message, params)

			gomega.Expect(err).To(gomega.HaveOccurred())
			gomega.Expect(err).To(gomega.MatchError(ErrEmptyMessage))
		})

		ginkgo.It("should handle large message with chunking", func() {
			mockClient := mocks.NewMockHTTPClient(ginkgo.GinkgoT())
			service.HTTPClient = mockClient
			mockClient.On("Do", mock.Anything).Return(&http.Response{
				StatusCode: http.StatusNoContent,
				Body:       io.NopCloser(strings.NewReader("")),
			}, nil)

			// Create a message larger than TotalChunkSize to force multiple batches
			largeMessage := strings.Repeat("a", TotalChunkSize+100)
			params := &types.Params{}

			err := service.Send(largeMessage, params)

			gomega.Expect(err).ToNot(gomega.HaveOccurred())
			// Should make multiple calls for chunks
			mockClient.AssertNumberOfCalls(ginkgo.GinkgoT(), "Do", 2)
		})

		ginkgo.It("should handle split lines option", func() {
			mockClient := mocks.NewMockHTTPClient(ginkgo.GinkgoT())
			service.HTTPClient = mockClient
			service.Config.SplitLines = true

			mockClient.On("Do", mock.Anything).Return(&http.Response{
				StatusCode: http.StatusNoContent,
				Body:       io.NopCloser(strings.NewReader("")),
			}, nil)

			message := "Line 1\nLine 2\nLine 3"
			params := &types.Params{}

			err := service.Send(message, params)

			gomega.Expect(err).ToNot(gomega.HaveOccurred())
			// One call for the batch of lines
			mockClient.AssertNumberOfCalls(ginkgo.GinkgoT(), "Do", 1)
		})

		ginkgo.It("should return error when first batch fails", func() {
			mockClient := mocks.NewMockHTTPClient(ginkgo.GinkgoT())
			mockLogger := &mockTypes.MockStdLogger{}
			service.HTTPClient = mockClient
			service.SetLogger(mockLogger)
			mockClient.On("Do", mock.Anything).Return(nil, ErrMaxRetries)
			mockLogger.On("Print", mock.Anything).Return()

			message := "Test message"
			params := &types.Params{}

			err := service.Send(message, params)

			gomega.Expect(err).To(gomega.HaveOccurred())
			gomega.Expect(err.Error()).
				To(gomega.ContainSubstring("failed to send discord notification"))
		})
	})

	ginkgo.Describe("Service.SendItems method", func() {
		ginkgo.It("should delegate to sendItems method", func() {
			mockClient := mocks.NewMockHTTPClient(ginkgo.GinkgoT())
			service := &Service{
				Config: &Config{
					WebhookID: "123456789",
					Token:     "test-token",
				},
				HTTPClient: mockClient,
			}
			mockClient.On("Do", mock.Anything).Return(&http.Response{
				StatusCode: http.StatusNoContent,
				Body:       io.NopCloser(strings.NewReader("")),
			}, nil)

			items := []types.MessageItem{
				{Text: "Test message"},
			}
			params := &types.Params{}

			err := service.SendItems(items, params)

			gomega.Expect(err).ToNot(gomega.HaveOccurred())
		})
	})

	ginkgo.Describe("Service.sendItems method", func() {
		var service *Service

		ginkgo.BeforeEach(func() {
			service = &Service{
				Config: &Config{
					WebhookID: "123456789",
					Token:     "test-token",
				},
				pkr: format.NewPropKeyResolver(&Config{}),
			}
		})

		ginkgo.It("should send simple message item successfully", func() {
			mockClient := mocks.NewMockHTTPClient(ginkgo.GinkgoT())
			service.HTTPClient = mockClient
			mockClient.On("Do", mock.Anything).Return(&http.Response{
				StatusCode: http.StatusNoContent,
				Body:       io.NopCloser(strings.NewReader("")),
			}, nil)

			items := []types.MessageItem{
				{Text: "Simple message"},
			}
			params := &types.Params{}

			err := service.sendItems(items, params)

			gomega.Expect(err).ToNot(gomega.HaveOccurred())
			mockClient.AssertNumberOfCalls(ginkgo.GinkgoT(), "Do", 1)
		})

		ginkgo.It("should handle message with file attachment", func() {
			mockClient := mocks.NewMockHTTPClient(ginkgo.GinkgoT())
			service.HTTPClient = mockClient
			mockClient.On("Do", mock.Anything).Return(&http.Response{
				StatusCode: http.StatusNoContent,
				Body:       io.NopCloser(strings.NewReader("")),
			}, nil)

			items := []types.MessageItem{
				{
					Text: "Message with file",
					File: &types.File{
						Name: "test.txt",
						Data: []byte("file content"),
					},
				},
			}
			params := &types.Params{}

			err := service.sendItems(items, params)

			gomega.Expect(err).ToNot(gomega.HaveOccurred())
			mockClient.AssertNumberOfCalls(ginkgo.GinkgoT(), "Do", 1)
		})

		ginkgo.It("should update config from params", func() {
			mockClient := mocks.NewMockHTTPClient(ginkgo.GinkgoT())
			service.HTTPClient = mockClient
			service.pkr = format.NewPropKeyResolver(service.Config)

			mockClient.On("Do", mock.Anything).Return(&http.Response{
				StatusCode: http.StatusNoContent,
				Body:       io.NopCloser(strings.NewReader("")),
			}, nil)

			items := []types.MessageItem{
				{Text: "Test message"},
			}
			params := &types.Params{
				"username": "CustomBot",
			}

			err := service.sendItems(items, params)

			gomega.Expect(err).ToNot(gomega.HaveOccurred())
			// The payload should include the updated username
		})

		ginkgo.It("should handle empty items", func() {
			items := []types.MessageItem{}
			params := &types.Params{}

			err := service.sendItems(items, params)

			gomega.Expect(err).To(gomega.HaveOccurred())
			gomega.Expect(err).To(gomega.MatchError(ErrEmptyMessage))
		})

		ginkgo.It("should handle config update error", func() {
			service.pkr = format.NewPropKeyResolver(service.Config)

			// Use invalid params to trigger an error
			items := []types.MessageItem{
				{Text: "Test message"},
			}
			params := &types.Params{
				"invalid_key": "value",
			}

			err := service.sendItems(items, params)

			gomega.Expect(err).To(gomega.HaveOccurred())
			gomega.Expect(err.Error()).To(gomega.ContainSubstring("updating config from params"))
		})
	})

	ginkgo.Describe("CreateItemsFromPlain function", func() {
		ginkgo.It("should create single batch for small message without split lines", func() {
			plain := "Short message"
			splitLines := false

			result := CreateItemsFromPlain(plain, splitLines)

			gomega.Expect(result).To(gomega.HaveLen(1))
			gomega.Expect(result[0]).To(gomega.HaveLen(1))
			gomega.Expect(result[0][0].Text).To(gomega.Equal(plain))
		})

		ginkgo.It("should split lines when splitLines is true", func() {
			plain := "Line 1\nLine 2\nLine 3"
			splitLines := true

			result := CreateItemsFromPlain(plain, splitLines)

			gomega.Expect(result).To(gomega.HaveLen(1)) // All lines fit in one batch
			gomega.Expect(result[0]).To(gomega.HaveLen(3))
			gomega.Expect(result[0][0].Text).To(gomega.Equal("Line 1"))
			gomega.Expect(result[0][1].Text).To(gomega.Equal("Line 2"))
			gomega.Expect(result[0][2].Text).To(gomega.Equal("Line 3"))
		})

		ginkgo.It("should chunk large message without split lines", func() {
			largeMessage := strings.Repeat("a", ChunkSize+100)
			splitLines := false

			result := CreateItemsFromPlain(largeMessage, splitLines)

			gomega.Expect(result).To(gomega.HaveLen(1))    // All chunks fit in one batch
			gomega.Expect(result[0]).To(gomega.HaveLen(2)) // 2 chunks: 2000 and 100

			totalLength := 0
			for _, item := range result[0] {
				totalLength += len(item.Text)
			}

			gomega.Expect(totalLength).To(gomega.Equal(len(largeMessage)))
		})

		ginkgo.It("should handle empty string", func() {
			plain := ""
			splitLines := false

			result := CreateItemsFromPlain(plain, splitLines)

			gomega.Expect(result).To(gomega.HaveLen(1))
			gomega.Expect(result[0]).To(gomega.BeEmpty()) // Empty input returns empty items
		})

		ginkgo.It("should handle message exactly at chunk size", func() {
			exactMessage := strings.Repeat("a", ChunkSize)
			splitLines := false

			result := CreateItemsFromPlain(exactMessage, splitLines)

			gomega.Expect(result).To(gomega.HaveLen(1))
			gomega.Expect(result[0][0].Text).To(gomega.Equal(exactMessage))
		})
	})

	ginkgo.Describe("Service.Initialize method", func() {
		var service *Service

		ginkgo.BeforeEach(func() {
			service = &Service{}
		})

		ginkgo.It("should initialize service successfully with valid URL", func() {
			serviceURL, _ := url.Parse("discord://token@webhook?username=TestBot")
			logger := &mockTypes.MockStdLogger{}

			err := service.Initialize(serviceURL, logger)

			gomega.Expect(err).ToNot(gomega.HaveOccurred())
			gomega.Expect(service.Config).NotTo(gomega.BeNil())
			gomega.Expect(service.Config.WebhookID).To(gomega.Equal("webhook"))
			gomega.Expect(service.Config.Token).To(gomega.Equal("token"))
			gomega.Expect(service.Config.Username).To(gomega.Equal("TestBot"))
			gomega.Expect(service.HTTPClient).NotTo(gomega.BeNil())
		})

		ginkgo.It("should handle URL parsing error", func() {
			// Simulate config.SetURL error by using invalid URL
			invalidURL, _ := url.Parse("discord://@") // Missing token
			logger := &mockTypes.MockStdLogger{}

			err := service.Initialize(invalidURL, logger)

			gomega.Expect(err).To(gomega.HaveOccurred())
		})

		ginkgo.It("should set default properties", func() {
			serviceURL, _ := url.Parse("discord://token@webhook")
			logger := &mockTypes.MockStdLogger{}

			err := service.Initialize(serviceURL, logger)

			gomega.Expect(err).ToNot(gomega.HaveOccurred())
			// Verify default properties are set
			gomega.Expect(service.pkr).NotTo(gomega.BeNil())
		})
	})

	ginkgo.Describe("Service.GetID method", func() {
		var service *Service

		ginkgo.BeforeEach(func() {
			service = &Service{}
		})

		ginkgo.It("should return discord scheme", func() {
			id := service.GetID()

			gomega.Expect(id).To(gomega.Equal(Scheme))
			gomega.Expect(id).To(gomega.Equal("discord"))
		})
	})

	ginkgo.Describe("Service.doSend method", func() {
		var service *Service

		ginkgo.BeforeEach(func() {
			service = &Service{}
		})

		ginkgo.It("should send payload successfully", func() {
			mockClient := mocks.NewMockHTTPClient(ginkgo.GinkgoT())
			service.HTTPClient = mockClient
			mockClient.On("Do", mock.Anything).Return(&http.Response{
				StatusCode: http.StatusNoContent,
				Body:       io.NopCloser(strings.NewReader("")),
			}, nil)

			payload := []byte(`{"content":"test"}`)
			postURL := testWebhookURLAlt

			err := service.doSend(payload, postURL)

			gomega.Expect(err).ToNot(gomega.HaveOccurred())
			mockClient.AssertNumberOfCalls(ginkgo.GinkgoT(), "Do", 1)
		})

		ginkgo.It("should handle empty URL", func() {
			payload := []byte(`{"content":"test"}`)
			postURL := ""

			err := service.doSend(payload, postURL)

			gomega.Expect(err).To(gomega.MatchError(ErrEmptyURL))
		})

		ginkgo.It("should validate URL scheme", func() {
			payload := []byte(`{"content":"test"}`)
			postURL := "http://" + testWebhookURLAlt[8:] // Change https to http

			err := service.doSend(payload, postURL)

			gomega.Expect(err).To(gomega.MatchError(ErrInvalidScheme))
		})

		ginkgo.It("should validate host", func() {
			payload := []byte(`{"content":"test"}`)
			postURL := "https://example.com" + testWebhookURLAlt[20:] // Replace discord.com with example.com

			err := service.doSend(payload, postURL)

			gomega.Expect(err).To(gomega.MatchError(ErrInvalidHost))
		})

		ginkgo.It("should validate URL prefix", func() {
			payload := []byte(`{"content":"test"}`)
			postURL := "https://discord.com/api/invalid" + testWebhookURLAlt[28:] // Replace /webhooks with /invalid

			err := service.doSend(payload, postURL)

			gomega.Expect(err).To(gomega.MatchError(ErrInvalidURLPrefix))
		})

		ginkgo.It("should validate webhook ID and token", func() {
			payload := []byte(`{"content":"test"}`)
			postURL := "https://discord.com/api/webhooks//"

			err := service.doSend(payload, postURL)

			gomega.Expect(err).To(gomega.MatchError(ErrMalformedURL))
		})

		ginkgo.It("should handle HTTP client error", func() {
			mockClient := mocks.NewMockHTTPClient(ginkgo.GinkgoT())
			service.HTTPClient = mockClient
			mockClient.On("Do", mock.Anything).Return(nil, errors.New("network error"))

			payload := []byte(`{"content":"test"}`)
			postURL := testWebhookURLAlt

			err := service.doSend(payload, postURL)

			gomega.Expect(err).To(gomega.HaveOccurred())
		})
	})

	ginkgo.Describe("Service.doSendMultipart method", func() {
		var service *Service

		ginkgo.BeforeEach(func() {
			service = &Service{}
		})

		ginkgo.It("should send multipart payload with files successfully", func() {
			mockClient := mocks.NewMockHTTPClient(ginkgo.GinkgoT())
			service.HTTPClient = mockClient
			mockClient.On("Do", mock.Anything).Return(&http.Response{
				StatusCode: http.StatusNoContent,
				Body:       io.NopCloser(strings.NewReader("")),
			}, nil)

			payload := WebhookPayload{
				Content: "Message with files",
			}
			files := []types.File{
				{Name: "test.txt", Data: []byte("file content")},
			}
			postURL := testWebhookURLAlt

			err := service.doSendMultipart(&payload, files, postURL)

			gomega.Expect(err).ToNot(gomega.HaveOccurred())
			mockClient.AssertNumberOfCalls(ginkgo.GinkgoT(), "Do", 1)
		})

		ginkgo.It("should handle empty files array", func() {
			mockClient := mocks.NewMockHTTPClient(ginkgo.GinkgoT())
			service.HTTPClient = mockClient
			mockClient.On("Do", mock.Anything).Return(&http.Response{
				StatusCode: http.StatusNoContent,
				Body:       io.NopCloser(strings.NewReader("")),
			}, nil)

			payload := WebhookPayload{
				Content: "Message without files",
			}
			files := []types.File{}
			postURL := testWebhookURLAlt

			err := service.doSendMultipart(&payload, files, postURL)

			gomega.Expect(err).ToNot(gomega.HaveOccurred())
			mockClient.AssertNumberOfCalls(ginkgo.GinkgoT(), "Do", 1)
		})

		ginkgo.It("should handle HTTP client error", func() {
			mockClient := mocks.NewMockHTTPClient(ginkgo.GinkgoT())
			service.HTTPClient = mockClient
			mockClient.On("Do", mock.Anything).Return(nil, errors.New("network error"))

			payload := WebhookPayload{
				Content: "Test message",
			}
			files := []types.File{}
			postURL := testWebhookURLAlt

			err := service.doSendMultipart(&payload, files, postURL)

			gomega.Expect(err).To(gomega.HaveOccurred())
		})
	})
})
