package services

import (
	"context"
	"database/sql"
	"errors"
	"testing"

	"quyun/v2/app/commands/testx"
	"quyun/v2/app/errorx"
	content_dto "quyun/v2/app/http/v1/dto"
	"quyun/v2/app/requests"
	"quyun/v2/database"
	"quyun/v2/database/models"
	"quyun/v2/pkg/consts"

	. "github.com/smartystreets/goconvey/convey"
	"github.com/stretchr/testify/suite"
	"go.ipao.vip/atom/contracts"
	"go.ipao.vip/gen/types"
	"go.uber.org/dig"
)

type ContentTestSuiteInjectParams struct {
	dig.In

	DB       *sql.DB
	Initials []contracts.Initial `group:"initials"`
}

type ContentTestSuite struct {
	suite.Suite
	ContentTestSuiteInjectParams
}

func Test_Content(t *testing.T) {
	providers := testx.Default().With(Provide)

	testx.Serve(providers, t, func(p ContentTestSuiteInjectParams) {
		suite.Run(t, &ContentTestSuite{ContentTestSuiteInjectParams: p})
	})
}

func (s *ContentTestSuite) Test_List() {
	Convey("List", s.T(), func() {
		ctx := s.T().Context()
		tenantID := int64(1)
		database.Truncate(ctx, s.DB, models.TableNameContent, models.TableNameUser)

		// Create Author
		author := &models.User{Nickname: "Author1", Username: "author1", Phone: "13800000001"}
		models.UserQuery.WithContext(ctx).Create(author)

		// Create Contents
		c1 := &models.Content{
			TenantID: 1,
			UserID:   author.ID,
			Title:    "Content A",
			Status:   consts.ContentStatusPublished,
			Genre:    "video",
		}
		c2 := &models.Content{
			TenantID: 1,
			UserID:   author.ID,
			Title:    "Content B",
			Status:   consts.ContentStatusDraft, // Draft
			Genre:    "video",
		}
		models.ContentQuery.WithContext(ctx).Create(c1, c2)

		Convey("should list only published contents", func() {
			tid := int64(1)
			filter := &content_dto.ContentListFilter{
				TenantID: &tid,
				Pagination: requests.Pagination{
					Page:  1,
					Limit: 10,
				},
			}
			res, err := Content.List(ctx, tenantID, filter)
			So(err, ShouldBeNil)
			So(res.Total, ShouldEqual, 1)
			items := res.Items.([]content_dto.ContentItem)
			So(items[0].Title, ShouldEqual, "Content A")
			So(items[0].AuthorName, ShouldEqual, "Author1")
		})
	})
}

func (s *ContentTestSuite) Test_Get() {
	Convey("Get", s.T(), func() {
		ctx := s.T().Context()
		tenantID := int64(1)
		database.Truncate(ctx, s.DB, models.TableNameContent, models.TableNameMediaAsset, models.TableNameContentAsset, models.TableNameTenantUser, models.TableNameUser)

		// Author
		author := &models.User{Nickname: "Author1", Username: "author1", Phone: "13800000002"}
		models.UserQuery.WithContext(ctx).Create(author)

		// Asset
		asset := &models.MediaAsset{
			TenantID:  1,
			UserID:    author.ID,
			ObjectKey: "test.mp4",
			Type:      consts.MediaAssetTypeVideo,
		}
		models.MediaAssetQuery.WithContext(ctx).Create(asset)

		// Content
		content := &models.Content{
			TenantID: 1,
			UserID:   author.ID,
			Title:    "Detail Content",
			Status:   consts.ContentStatusPublished,
		}
		models.ContentQuery.WithContext(ctx).Create(content)

		member := &models.User{Nickname: "Member", Username: "member1", Phone: "13800000003"}
		admin := &models.User{Nickname: "Admin", Username: "admin1", Phone: "13800000005"}
		guest := &models.User{Nickname: "Guest", Username: "guest1", Phone: "13800000004"}
		models.UserQuery.WithContext(ctx).Create(member, admin, guest)
		models.TenantUserQuery.WithContext(ctx).Create(&models.TenantUser{
			TenantID: 1,
			UserID:   member.ID,
			Role:     types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember},
			Status:   consts.UserStatusVerified,
		})
		models.TenantUserQuery.WithContext(ctx).Create(&models.TenantUser{
			TenantID: 1,
			UserID:   admin.ID,
			Role:     types.Array[consts.TenantUserRole]{consts.TenantUserRoleTenantAdmin},
			Status:   consts.UserStatusVerified,
		})

		tenantOnly := &models.Content{
			TenantID:   1,
			UserID:     author.ID,
			Title:      "Member Only",
			Status:     consts.ContentStatusPublished,
			Visibility: consts.ContentVisibilityTenantOnly,
		}
		privateContent := &models.Content{
			TenantID:   1,
			UserID:     author.ID,
			Title:      "Private Content",
			Status:     consts.ContentStatusPublished,
			Visibility: consts.ContentVisibilityPrivate,
		}
		models.ContentQuery.WithContext(ctx).Create(tenantOnly, privateContent)

		// Link Asset
		ca := &models.ContentAsset{
			TenantID:  1,
			UserID:    author.ID,
			ContentID: content.ID,
			AssetID:   asset.ID,
			Sort:      1,
			Role:      consts.ContentAssetRoleMain, // Explicitly set role
		}
		models.ContentAssetQuery.WithContext(ctx).Create(ca)

		tenantMain := &models.MediaAsset{
			TenantID:  1,
			UserID:    author.ID,
			ObjectKey: "tenant_main.mp4",
			Type:      consts.MediaAssetTypeVideo,
		}
		tenantPrev := &models.MediaAsset{
			TenantID:  1,
			UserID:    author.ID,
			ObjectKey: "tenant_preview.mp4",
			Type:      consts.MediaAssetTypeVideo,
		}
		models.MediaAssetQuery.WithContext(ctx).Create(tenantMain, tenantPrev)
		models.ContentAssetQuery.WithContext(ctx).Create(
			&models.ContentAsset{
				TenantID:  1,
				UserID:    author.ID,
				ContentID: tenantOnly.ID,
				AssetID:   tenantMain.ID,
				Sort:      1,
				Role:      consts.ContentAssetRoleMain,
			},
			&models.ContentAsset{
				TenantID:  1,
				UserID:    author.ID,
				ContentID: tenantOnly.ID,
				AssetID:   tenantPrev.ID,
				Sort:      2,
				Role:      consts.ContentAssetRolePreview,
			},
		)

		// Set context to author
		ctx = context.WithValue(ctx, consts.CtxKeyUser, author.ID)

		Convey("should get detail with assets", func() {
			detail, err := Content.Get(ctx, tenantID, author.ID, content.ID)
			So(err, ShouldBeNil)
			So(detail.Title, ShouldEqual, "Detail Content")
			So(detail.AuthorName, ShouldEqual, "Author1")
			So(len(detail.MediaUrls), ShouldEqual, 1)
			So(detail.MediaUrls[0].URL, ShouldContainSubstring, "test.mp4")
		})

		Convey("should allow tenant_only content for member", func() {
			detail, err := Content.Get(ctx, tenantID, member.ID, tenantOnly.ID)
			So(err, ShouldBeNil)
			So(detail.Title, ShouldEqual, "Member Only")
			So(len(detail.MediaUrls), ShouldEqual, 2)
		})

		Convey("should allow tenant_only content for admin", func() {
			detail, err := Content.Get(ctx, tenantID, admin.ID, tenantOnly.ID)
			So(err, ShouldBeNil)
			So(detail.Title, ShouldEqual, "Member Only")
			So(len(detail.MediaUrls), ShouldEqual, 2)
		})

		Convey("should reject tenant_only content for non-member", func() {
			_, err := Content.Get(ctx, tenantID, guest.ID, tenantOnly.ID)
			So(err, ShouldNotBeNil)
			var appErr *errorx.AppError
			So(errors.As(err, &appErr), ShouldBeTrue)
			So(appErr.Code, ShouldEqual, errorx.ErrForbidden.Code)
		})

		Convey("should reject private content for non-owner", func() {
			_, err := Content.Get(ctx, tenantID, member.ID, privateContent.ID)
			So(err, ShouldNotBeNil)
			var appErr *errorx.AppError
			So(errors.As(err, &appErr), ShouldBeTrue)
			So(appErr.Code, ShouldEqual, errorx.ErrForbidden.Code)
		})

		Convey("should allow private content for author", func() {
			detail, err := Content.Get(ctx, tenantID, author.ID, privateContent.ID)
			So(err, ShouldBeNil)
			So(detail.Title, ShouldEqual, "Private Content")
		})
	})
}

func (s *ContentTestSuite) Test_CreateComment() {
	Convey("CreateComment", s.T(), func() {
		ctx := s.T().Context()
		tenantID := int64(1)
		database.Truncate(ctx, s.DB, models.TableNameContent, models.TableNameComment, models.TableNameUser)

		// User & Content
		u := &models.User{Username: "user1", Phone: "13900000001"}
		models.UserQuery.WithContext(ctx).Create(u)
		c := &models.Content{TenantID: 1, UserID: u.ID, Title: "C"}
		models.ContentQuery.WithContext(ctx).Create(c)

		// Auth context
		ctx = context.WithValue(ctx, consts.CtxKeyUser, u.ID)

		Convey("should create comment", func() {
			form := &content_dto.CommentCreateForm{
				Content: "Nice!",
			}
			err := Content.CreateComment(ctx, tenantID, u.ID, c.ID, form)
			So(err, ShouldBeNil)

			count, _ := models.CommentQuery.WithContext(ctx).Where(models.CommentQuery.ContentID.Eq(c.ID)).Count()
			So(count, ShouldEqual, 1)
		})
	})
}

func (s *ContentTestSuite) Test_Library() {
	Convey("Library", s.T(), func() {
		ctx := s.T().Context()
		tenantID := int64(1)
		database.Truncate(ctx, s.DB, models.TableNameContent, models.TableNameContentAccess, models.TableNameUser, models.TableNameContentAsset, models.TableNameMediaAsset)

		// User
		u := &models.User{Username: "user_lib", Phone: "13900000002"}
		models.UserQuery.WithContext(ctx).Create(u)
		ctx = context.WithValue(ctx, consts.CtxKeyUser, u.ID)

		// Content
		c := &models.Content{TenantID: 1, UserID: u.ID, Title: "Paid Content", Genre: "video"}
		models.ContentQuery.WithContext(ctx).Create(c)

		// Asset (Video & Cover)
		assetVid := &models.MediaAsset{TenantID: 1, UserID: u.ID, Type: consts.MediaAssetTypeVideo, ObjectKey: "video.mp4"}
		assetImg := &models.MediaAsset{TenantID: 1, UserID: u.ID, Type: consts.MediaAssetTypeImage, ObjectKey: "cover.jpg"}
		models.MediaAssetQuery.WithContext(ctx).Create(assetVid, assetImg)

		models.ContentAssetQuery.WithContext(ctx).Create(
			&models.ContentAsset{ContentID: c.ID, AssetID: assetVid.ID, Role: consts.ContentAssetRoleMain},
			&models.ContentAsset{ContentID: c.ID, AssetID: assetImg.ID, Role: consts.ContentAssetRoleCover},
		)

		// Access
		models.ContentAccessQuery.WithContext(ctx).Create(&models.ContentAccess{
			TenantID: 1, UserID: u.ID, ContentID: c.ID, Status: "active",
		})

		Convey("should get library content with details", func() {
			list, err := Content.GetLibrary(ctx, tenantID, u.ID)
			So(err, ShouldBeNil)
			So(len(list), ShouldEqual, 1)
			So(list[0].Title, ShouldEqual, "Paid Content")
			So(list[0].Type, ShouldEqual, "video")
			So(list[0].Cover, ShouldContainSubstring, "cover.jpg")
			So(list[0].IsPurchased, ShouldBeTrue)
		})
	})
}

func (s *ContentTestSuite) Test_Interact() {
	Convey("Interact", s.T(), func() {
		ctx := s.T().Context()
		tenantID := int64(1)
		database.Truncate(ctx, s.DB, models.TableNameContent, models.TableNameUserContentAction, models.TableNameUser)

		// User & Content
		u := &models.User{Username: "user_act", Phone: "13900000003"}
		models.UserQuery.WithContext(ctx).Create(u)
		c := &models.Content{TenantID: 1, UserID: u.ID, Title: "Liked Content", Likes: 0}
		models.ContentQuery.WithContext(ctx).Create(c)

		ctx = context.WithValue(ctx, consts.CtxKeyUser, u.ID)

		Convey("Like flow", func() {
			// Add Like
			err := Content.AddLike(ctx, tenantID, u.ID, c.ID)
			So(err, ShouldBeNil)

			// Verify count
			cReload, _ := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(c.ID)).First()
			So(cReload.Likes, ShouldEqual, 1)

			// Get Likes
			likes, err := Content.GetLikes(ctx, tenantID, u.ID)
			So(err, ShouldBeNil)
			So(len(likes), ShouldEqual, 1)
			So(likes[0].ID, ShouldEqual, c.ID)

			// Remove Like
			err = Content.RemoveLike(ctx, tenantID, u.ID, c.ID)
			So(err, ShouldBeNil)

			// Verify count
			cReload, _ = models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(c.ID)).First()
			So(cReload.Likes, ShouldEqual, 0)
		})

		Convey("Favorite flow", func() {
			// Add Favorite
			err := Content.AddFavorite(ctx, tenantID, u.ID, c.ID)
			So(err, ShouldBeNil)

			// Get Favorites
			favs, err := Content.GetFavorites(ctx, tenantID, u.ID)
			So(err, ShouldBeNil)
			So(len(favs), ShouldEqual, 1)
			So(favs[0].ID, ShouldEqual, c.ID)

			// Remove Favorite
			err = Content.RemoveFavorite(ctx, tenantID, u.ID, c.ID)
			So(err, ShouldBeNil)

			// Get Favorites
			favs, err = Content.GetFavorites(ctx, tenantID, u.ID)
			So(err, ShouldBeNil)
			So(len(favs), ShouldEqual, 0)
		})

		Convey("should reject interactions for unpublished content", func() {
			other := &models.User{Username: "user_act2", Phone: "13900000099"}
			models.UserQuery.WithContext(ctx).Create(other)

			draft := &models.Content{
				TenantID: 1,
				UserID:   u.ID,
				Title:    "Draft Content",
				Status:   consts.ContentStatusDraft,
			}
			models.ContentQuery.WithContext(ctx).Create(draft)

			err := Content.AddLike(ctx, tenantID, other.ID, draft.ID)
			So(err, ShouldNotBeNil)

			err = Content.AddFavorite(ctx, tenantID, other.ID, draft.ID)
			So(err, ShouldNotBeNil)
		})
	})
}

func (s *ContentTestSuite) Test_ListTopics() {
	Convey("ListTopics", s.T(), func() {
		ctx := s.T().Context()
		tenantID := int64(1)
		database.Truncate(ctx, s.DB, models.TableNameContent, models.TableNameUser)

		u := &models.User{Username: "user_t", Phone: "13900000005"}
		models.UserQuery.WithContext(ctx).Create(u)

		// Create Contents: 2 video, 1 audio
		models.ContentQuery.WithContext(ctx).Create(
			&models.Content{TenantID: 1, UserID: u.ID, Title: "V1", Genre: "video", Status: consts.ContentStatusPublished},
			&models.Content{TenantID: 1, UserID: u.ID, Title: "V2", Genre: "video", Status: consts.ContentStatusPublished},
			&models.Content{TenantID: 1, UserID: u.ID, Title: "A1", Genre: "audio", Status: consts.ContentStatusPublished},
			&models.Content{TenantID: 1, UserID: u.ID, Title: "D1", Genre: "draft", Status: consts.ContentStatusDraft}, // Should ignore
		)

		Convey("should aggregate topics", func() {
			topics, err := Content.ListTopics(ctx, tenantID)
			So(err, ShouldBeNil)
			So(len(topics), ShouldBeGreaterThanOrEqualTo, 2)

			var videoCount, audioCount int
			for _, t := range topics {
				if t.Tag == "video" {
					videoCount = t.Count
				}
				if t.Tag == "audio" {
					audioCount = t.Count
				}
			}
			So(videoCount, ShouldEqual, 2)
			So(audioCount, ShouldEqual, 1)
		})
	})
}

func (s *ContentTestSuite) Test_PreviewLogic() {
	Convey("Preview Logic", s.T(), func() {
		ctx := s.T().Context()
		tenantID := int64(1)
		database.Truncate(ctx, s.DB, models.TableNameContent, models.TableNameContentAsset, models.TableNameContentAccess, models.TableNameUser, models.TableNameMediaAsset)

		author := &models.User{Username: "author_p", Phone: "13900000006"}
		models.UserQuery.WithContext(ctx).Create(author)

		c := &models.Content{
			TenantID:   1,
			UserID:     author.ID,
			Title:      "Premium",
			Status:     consts.ContentStatusPublished,
			Visibility: consts.ContentVisibilityPublic,
		}
		models.ContentQuery.WithContext(ctx).Create(c)

		assetMain := &models.MediaAsset{ObjectKey: "main.mp4", Type: consts.MediaAssetTypeVideo}
		assetPrev := &models.MediaAsset{ObjectKey: "preview.mp4", Type: consts.MediaAssetTypeVideo}
		models.MediaAssetQuery.WithContext(ctx).Create(assetMain, assetPrev)

		models.ContentAssetQuery.WithContext(ctx).Create(
			&models.ContentAsset{ContentID: c.ID, AssetID: assetMain.ID, Role: consts.ContentAssetRoleMain},
			&models.ContentAsset{ContentID: c.ID, AssetID: assetPrev.ID, Role: consts.ContentAssetRolePreview},
		)

		Convey("guest should see preview only", func() {
			guest := &models.User{Username: "guest", Phone: "13900000007"}
			models.UserQuery.WithContext(ctx).Create(guest)
			guestCtx := context.WithValue(ctx, consts.CtxKeyUser, guest.ID)

			detail, err := Content.Get(guestCtx, tenantID, 0, c.ID)
			So(err, ShouldBeNil)
			So(len(detail.MediaUrls), ShouldEqual, 1)
			So(detail.MediaUrls[0].URL, ShouldContainSubstring, "preview.mp4")
			So(detail.IsPurchased, ShouldBeFalse)
		})

		Convey("owner should see all", func() {
			ownerCtx := context.WithValue(ctx, consts.CtxKeyUser, author.ID)
			detail, err := Content.Get(ownerCtx, tenantID, author.ID, c.ID)
			So(err, ShouldBeNil)
			So(len(detail.MediaUrls), ShouldEqual, 2)
			So(detail.IsPurchased, ShouldBeTrue)
		})

		Convey("buyer should see all", func() {
			buyer := &models.User{Username: "buyer_p", Phone: "13900000008"}
			models.UserQuery.WithContext(ctx).Create(buyer)
			buyerCtx := context.WithValue(ctx, consts.CtxKeyUser, buyer.ID)

			models.ContentAccessQuery.WithContext(ctx).Create(&models.ContentAccess{
				UserID: buyer.ID, ContentID: c.ID, Status: consts.ContentAccessStatusActive,
			})

			detail, err := Content.Get(buyerCtx, tenantID, buyer.ID, c.ID)
			So(err, ShouldBeNil)
			So(len(detail.MediaUrls), ShouldEqual, 2)
			So(detail.IsPurchased, ShouldBeTrue)
		})
	})
}

func (s *ContentTestSuite) Test_ViewCounting() {
	Convey("ViewCounting", s.T(), func() {
		ctx := s.T().Context()
		tenantID := int64(1)
		database.Truncate(ctx, s.DB, models.TableNameContent, models.TableNameUser)

		author := &models.User{Username: "author_v", Phone: "13900000009"}
		models.UserQuery.WithContext(ctx).Create(author)

		c := &models.Content{
			TenantID:   1,
			UserID:     author.ID,
			Title:      "View Me",
			Views:      0,
			Status:     consts.ContentStatusPublished,
			Visibility: consts.ContentVisibilityPublic,
		}
		models.ContentQuery.WithContext(ctx).Create(c)

		Convey("should increment views", func() {
			_, err := Content.Get(ctx, tenantID, 0, c.ID)
			So(err, ShouldBeNil)

			cReload, _ := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(c.ID)).First()
			So(cReload.Views, ShouldEqual, 1)
		})
	})
}
