package handler

import (
	"context"
	"database/sql"
	"net/http/httptest"
	"testing"

	"github.com/brianvoe/gofakeit/v6"
	"github.com/gofiber/fiber/v2"
	"github.com/gojuno/minimock/v3"
	"github.com/stretchr/testify/assert"

	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/helper/test"
	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/model"
	"git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/service/handler/mocks"
)

func TestArticleHandler(t *testing.T) {
	t.Parallel()

	type req struct {
		method string
		route  string
	}

	var (
		articleID   = gofakeit.Uint64()
		articleURL  = gofakeit.Word()
		publishTime = gofakeit.Date()
		internalErr = gofakeit.Error()
		language    = model.LanguageID(gofakeit.Uint64())

		article = model.Article{
			ID:          articleID,
			URL:         articleURL,
			Title:       gofakeit.Phrase(),
			Text:        gofakeit.Phrase(),
			PublishTime: publishTime,
			PreviewText: sql.NullString{Valid: true, String: gofakeit.Phrase()},
			Image:       gofakeit.URL(),
			Language:    language,
			IsActive:    true,
		}

		notActiveArticle = model.Article{
			ID:          articleID,
			URL:         articleURL,
			Title:       gofakeit.Phrase(),
			Text:        gofakeit.Phrase(),
			PublishTime: publishTime,
			PreviewText: sql.NullString{Valid: true, String: gofakeit.Phrase()},
			Image:       gofakeit.URL(),
			Language:    language,
		}

		tags = []model.Tag{
			{
				ID:  gofakeit.Uint64(),
				Tag: gofakeit.Word(),
				URL: gofakeit.Word(),
			},
			{
				ID:  gofakeit.Uint64(),
				Tag: gofakeit.Word(),
				URL: gofakeit.Word(),
			},
		}

		previewArticles = []model.ArticlePreview{
			{
				ID:          gofakeit.Uint64(),
				URL:         gofakeit.URL(),
				Title:       gofakeit.Phrase(),
				PublishTime: publishTime,
				PreviewText: sql.NullString{Valid: true, String: gofakeit.Phrase()},
				Image:       gofakeit.URL(),
			},
			{
				ID:          gofakeit.Uint64(),
				URL:         gofakeit.URL(),
				Title:       gofakeit.Phrase(),
				PublishTime: publishTime,
				PreviewText: sql.NullString{Valid: true, String: gofakeit.Phrase()},
				Image:       gofakeit.URL(),
			},
			{
				ID:          gofakeit.Uint64(),
				URL:         gofakeit.URL(),
				Title:       gofakeit.Phrase(),
				PublishTime: publishTime,
				PreviewText: sql.NullString{Valid: true, String: gofakeit.Phrase()},
				Image:       gofakeit.URL(),
			},
			{
				ID:          gofakeit.Uint64(),
				URL:         gofakeit.URL(),
				Title:       gofakeit.Phrase(),
				PublishTime: publishTime,
				PreviewText: sql.NullString{Valid: true, String: gofakeit.Phrase()},
				Image:       gofakeit.URL(),
			},
		}
	)

	tests := []struct {
		name        string
		req         req
		res         int
		err         error
		cacheMock   func(mc *minimock.Controller) CacheService
		tagMock     func(mc *minimock.Controller) TagRepository
		articleMock func(mc *minimock.Controller) ArticleRepository
	}{
		{
			name: "positive case",
			req: req{
				method: fiber.MethodGet,
				route:  "/article/" + articleURL,
			},
			res: fiber.StatusOK,
			err: nil,
			cacheMock: func(mc *minimock.Controller) CacheService {
				mock := mocks.NewCacheServiceMock(mc)

				mock.GetMock.When(ArticleCacheKey+articleURL).Then(nil, false)
				mock.GetMock.When(RecentArticlesCacheKey).Then(nil, false)
				mock.GetMock.When(UsedTagsCacheKey).Then(nil, false)
				mock.SetMock.Return()

				return mock
			},
			tagMock: func(mc *minimock.Controller) TagRepository {
				mock := mocks.NewTagRepositoryMock(mc)
				mock.GetAllUsedMock.Return(tags, nil)
				return mock
			},
			articleMock: func(mc *minimock.Controller) ArticleRepository {
				mock := mocks.NewArticleRepositoryMock(mc)

				mock.GetByURLMock.Inspect(func(ctx context.Context, url string, lang model.LanguageID) {
					assert.Equal(mc, articleURL, url)
				}).Return(&article, nil)

				mock.GetAllPreviewMock.Return(previewArticles, nil)

				return mock
			},
		},
		{
			name: "negative case - article not found",
			req: req{
				method: fiber.MethodGet,
				route:  "/article/" + articleURL,
			},
			res: fiber.StatusNotFound,
			err: nil,
			cacheMock: func(mc *minimock.Controller) CacheService {
				mock := mocks.NewCacheServiceMock(mc)
				mock.GetMock.Expect(ArticleCacheKey+articleURL).Return(nil, false)
				return mock
			},
			tagMock: func(mc *minimock.Controller) TagRepository {
				return mocks.NewTagRepositoryMock(mc)
			},
			articleMock: func(mc *minimock.Controller) ArticleRepository {
				mock := mocks.NewArticleRepositoryMock(mc)

				mock.GetByURLMock.Inspect(func(ctx context.Context, url string, lang model.LanguageID) {
					assert.Equal(mc, articleURL, url)
				}).Return(nil, sql.ErrNoRows)

				return mock
			},
		},
		{
			name: "negative case - article repository error",
			req: req{
				method: fiber.MethodGet,
				route:  "/article/" + articleURL,
			},
			res: fiber.StatusInternalServerError,
			err: nil,
			cacheMock: func(mc *minimock.Controller) CacheService {
				mock := mocks.NewCacheServiceMock(mc)
				mock.GetMock.Expect(ArticleCacheKey+articleURL).Return(nil, false)
				return mock
			},
			tagMock: func(mc *minimock.Controller) TagRepository {
				return mocks.NewTagRepositoryMock(mc)
			},
			articleMock: func(mc *minimock.Controller) ArticleRepository {
				mock := mocks.NewArticleRepositoryMock(mc)

				mock.GetByURLMock.Inspect(func(ctx context.Context, url string, lang model.LanguageID) {
					assert.Equal(mc, articleURL, url)
				}).Return(nil, internalErr)

				return mock
			},
		},
		{
			name: "negative case - article not active",
			req: req{
				method: fiber.MethodGet,
				route:  "/article/" + articleURL,
			},
			res: fiber.StatusNotFound,
			err: nil,
			cacheMock: func(mc *minimock.Controller) CacheService {
				mock := mocks.NewCacheServiceMock(mc)
				mock.GetMock.Expect(ArticleCacheKey+articleURL).Return(nil, false)
				return mock
			},
			tagMock: func(mc *minimock.Controller) TagRepository {
				return mocks.NewTagRepositoryMock(mc)
			},
			articleMock: func(mc *minimock.Controller) ArticleRepository {
				mock := mocks.NewArticleRepositoryMock(mc)
				mock.GetByURLMock.Inspect(func(ctx context.Context, url string, lang model.LanguageID) {
					assert.Equal(mc, articleURL, url)
				}).Return(&notActiveArticle, nil)

				return mock
			},
		},
		{
			name: "negative case - articles repository error",
			req: req{
				method: fiber.MethodGet,
				route:  "/article/" + articleURL,
			},
			res: fiber.StatusInternalServerError,
			err: nil,
			cacheMock: func(mc *minimock.Controller) CacheService {
				mock := mocks.NewCacheServiceMock(mc)

				mock.GetMock.When(ArticleCacheKey+articleURL).Then(nil, false)
				mock.GetMock.When(RecentArticlesCacheKey).Then(nil, false)
				mock.SetMock.Return()

				return mock
			},
			tagMock: func(mc *minimock.Controller) TagRepository {
				return mocks.NewTagRepositoryMock(mc)
			},
			articleMock: func(mc *minimock.Controller) ArticleRepository {
				mock := mocks.NewArticleRepositoryMock(mc)

				mock.GetByURLMock.Inspect(func(ctx context.Context, url string, lang model.LanguageID) {
					assert.Equal(mc, articleURL, url)
				}).Return(&article, nil)

				mock.GetAllPreviewMock.Return(nil, internalErr)

				return mock
			},
		},
		{
			name: "negative case - tags repository error",
			req: req{
				method: fiber.MethodGet,
				route:  "/article/" + articleURL,
			},
			res: fiber.StatusInternalServerError,
			err: nil,
			cacheMock: func(mc *minimock.Controller) CacheService {
				mock := mocks.NewCacheServiceMock(mc)

				mock.GetMock.When(ArticleCacheKey+articleURL).Then(nil, false)
				mock.GetMock.When(RecentArticlesCacheKey).Then(nil, false)
				mock.GetMock.When(UsedTagsCacheKey).Then(nil, false)
				mock.SetMock.Return()

				return mock
			},
			tagMock: func(mc *minimock.Controller) TagRepository {
				mock := mocks.NewTagRepositoryMock(mc)
				mock.GetAllUsedMock.Return(nil, internalErr)
				return mock
			},
			articleMock: func(mc *minimock.Controller) ArticleRepository {
				mock := mocks.NewArticleRepositoryMock(mc)

				mock.GetByURLMock.Inspect(func(ctx context.Context, url string, lang model.LanguageID) {
					assert.Equal(mc, articleURL, url)
				}).Return(&article, nil)

				mock.GetAllPreviewMock.Return(previewArticles, nil)

				return mock
			},
		},
	}

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

			mc := minimock.NewController(t)
			fiberApp := fiber.New(test.GetFiberTestConfig())
			fiberReq := httptest.NewRequest(tt.req.method, tt.req.route, nil)

			fiberApp.Get("/article/:article", NewArticlePageHandler(
				tt.cacheMock(mc),
				tt.articleMock(mc),
				tt.tagMock(mc),
			))

			fiberRes, fiberErr := fiberApp.Test(fiberReq)
			assert.Equal(t, tt.res, fiberRes.StatusCode)
			assert.Equal(t, tt.err, fiberErr)
		})
	}
}
