package services

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

	"quyun/v2/app/commands/testx"
	creator_dto "quyun/v2/app/http/v1/dto"
	"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.uber.org/dig"
)

type CreatorTestSuiteInjectParams struct {
	dig.In

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

type CreatorTestSuite struct {
	suite.Suite
	CreatorTestSuiteInjectParams
}

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

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

func (s *CreatorTestSuite) Test_Apply() {
	Convey("Apply", s.T(), func() {
		ctx := s.T().Context()
		tenantID := int64(0)
		database.Truncate(ctx, s.DB, models.TableNameTenant, models.TableNameTenantUser, models.TableNameUser)

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

		Convey("should create tenant", func() {
			form := &creator_dto.ApplyForm{
				Name: "My Channel",
			}
			err := Creator.Apply(ctx, tenantID, u.ID, form)
			So(err, ShouldBeNil)

			t, _ := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.UserID.Eq(u.ID)).First()
			So(t, ShouldNotBeNil)
			So(t.Name, ShouldEqual, "My Channel")
			So(t.Status, ShouldEqual, consts.TenantStatusPendingVerify)

			// Check admin role
			tu, _ := models.TenantUserQuery.WithContext(ctx).Where(models.TenantUserQuery.TenantID.Eq(t.ID)).First()
			So(tu, ShouldNotBeNil)
			// Role is array, check contains? Or first element?
			// types.Array is likely []T.
			So(len(tu.Role), ShouldEqual, 1)
			So(tu.Role[0], ShouldEqual, consts.TenantUserRoleTenantAdmin)
		})
	})
}

func (s *CreatorTestSuite) Test_CreateContent() {
	Convey("CreateContent", s.T(), func() {
		ctx := s.T().Context()
		tenantID := int64(0)
		database.Truncate(
			ctx,
			s.DB,
			models.TableNameTenant,
			models.TableNameContent,
			models.TableNameContentAsset,
			models.TableNameContentPrice,
			models.TableNameUser,
		)

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

		// Create Tenant manually
		t := &models.Tenant{UserID: u.ID, Name: "Channel 2", Code: "123", Status: consts.TenantStatusVerified}
		models.TenantQuery.WithContext(ctx).Create(t)
		tenantID = t.ID

		Convey("should create content and assets", func() {
			form := &creator_dto.ContentCreateForm{
				Title: "New Song",
				Genre: "audio",
				Price: 9.99,
				// MediaIDs: ... need media asset
			}
			err := Creator.CreateContent(ctx, tenantID, u.ID, form)
			So(err, ShouldBeNil)

			c, _ := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.Title.Eq("New Song")).First()
			So(c, ShouldNotBeNil)
			So(c.UserID, ShouldEqual, u.ID)
			So(c.TenantID, ShouldEqual, t.ID)

			// Check Price
			p, _ := models.ContentPriceQuery.WithContext(ctx).Where(models.ContentPriceQuery.ContentID.Eq(c.ID)).First()
			So(p, ShouldNotBeNil)
			So(p.PriceAmount, ShouldEqual, 999)
		})
	})
}

func (s *CreatorTestSuite) Test_UpdateContent() {
	Convey("UpdateContent", s.T(), func() {
		ctx := s.T().Context()
		tenantID := int64(0)
		database.Truncate(
			ctx,
			s.DB,
			models.TableNameTenant,
			models.TableNameContent,
			models.TableNameContentAsset,
			models.TableNameContentPrice,
			models.TableNameUser,
		)

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

		t := &models.Tenant{UserID: u.ID, Name: "Channel 3", Code: "124", Status: consts.TenantStatusVerified}
		models.TenantQuery.WithContext(ctx).Create(t)
		tenantID = t.ID

		c := &models.Content{TenantID: t.ID, UserID: u.ID, Title: "Old Title", Genre: "audio"}
		models.ContentQuery.WithContext(ctx).Create(c)
		models.ContentPriceQuery.WithContext(ctx).
			Create(&models.ContentPrice{TenantID: t.ID, UserID: u.ID, ContentID: c.ID, PriceAmount: 100})

		Convey("should update content", func() {
			price := 20.00
			form := &creator_dto.ContentUpdateForm{
				Title: "New Title",
				Genre: "video",
				Price: &price,
			}
			err := Creator.UpdateContent(ctx, tenantID, u.ID, c.ID, form)
			So(err, ShouldBeNil)

			// Verify
			cReload, _ := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(c.ID)).First()
			So(cReload.Title, ShouldEqual, "New Title")
			So(cReload.Genre, ShouldEqual, "video")

			p, _ := models.ContentPriceQuery.WithContext(ctx).Where(models.ContentPriceQuery.ContentID.Eq(c.ID)).First()
			So(p.PriceAmount, ShouldEqual, 2000)
		})
	})
}

func (s *CreatorTestSuite) Test_Dashboard() {
	Convey("Dashboard", s.T(), func() {
		ctx := s.T().Context()
		tenantID := int64(0)
		database.Truncate(
			ctx,
			s.DB,
			models.TableNameTenant,
			models.TableNameTenantUser,
			models.TableNameTenantLedger,
			models.TableNameUser,
			models.TableNameOrder,
		)

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

		t := &models.Tenant{UserID: u.ID, Name: "Channel 4", Code: "125", Status: consts.TenantStatusVerified}
		models.TenantQuery.WithContext(ctx).Create(t)
		tenantID = t.ID

		// Mock Data
		// 1. Followers
		models.TenantUserQuery.WithContext(ctx).Create(
			&models.TenantUser{TenantID: t.ID, UserID: 100},
			&models.TenantUser{TenantID: t.ID, UserID: 101},
		)

		// 2. Revenue (Ledgers)
		models.TenantLedgerQuery.WithContext(ctx).Create(
			&models.TenantLedger{TenantID: t.ID, Type: consts.TenantLedgerTypeDebitPurchase, Amount: 1000}, // 10.00
			&models.TenantLedger{TenantID: t.ID, Type: consts.TenantLedgerTypeDebitPurchase, Amount: 2000}, // 20.00
			&models.TenantLedger{
				TenantID: t.ID,
				Type:     consts.TenantLedgerTypeCreditRefund,
				Amount:   500,
			}, // -5.00 (Refund, currently Dashboard sums DebitPurchase only, ideally should subtract refunds, but let's stick to implementation)
		)

		Convey("should get stats", func() {
			stats, err := Creator.Dashboard(ctx, tenantID, u.ID)
			So(err, ShouldBeNil)
			So(stats.TotalFollowers.Value, ShouldEqual, 2)
			// Implementation sums 'debit_purchase' only based on my code
			So(stats.TotalRevenue.Value, ShouldEqual, 30.00)
		})
	})
}

func (s *CreatorTestSuite) Test_PayoutAccount() {
	Convey("PayoutAccount", s.T(), func() {
		ctx := s.T().Context()
		tenantID := int64(0)
		database.Truncate(ctx, s.DB, models.TableNameTenant, models.TableNamePayoutAccount, models.TableNameUser)

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

		t := &models.Tenant{UserID: u.ID, Name: "Channel 5", Code: "126", Status: consts.TenantStatusVerified}
		models.TenantQuery.WithContext(ctx).Create(t)
		tenantID = t.ID

		Convey("should CRUD payout account", func() {
			// Add
			form := &creator_dto.PayoutAccount{
				Type:     string(consts.PayoutAccountTypeAlipay),
				Name:     "Alipay",
				Account:  "user@example.com",
				Realname: "John Doe",
			}
			err := Creator.AddPayoutAccount(ctx, tenantID, u.ID, form)
			So(err, ShouldBeNil)

			// List
			list, err := Creator.ListPayoutAccounts(ctx, tenantID, u.ID)
			So(err, ShouldBeNil)
			So(len(list), ShouldEqual, 1)
			So(list[0].Account, ShouldEqual, "user@example.com")

			// Remove
			err = Creator.RemovePayoutAccount(ctx, tenantID, u.ID, list[0].ID)
			So(err, ShouldBeNil)

			// Verify Empty
			list, err = Creator.ListPayoutAccounts(ctx, tenantID, u.ID)
			So(err, ShouldBeNil)
			So(len(list), ShouldEqual, 0)
		})
	})
}

func (s *CreatorTestSuite) Test_Withdraw() {
	Convey("Withdraw", s.T(), func() {
		ctx := s.T().Context()
		tenantID := int64(0)
		database.Truncate(
			ctx,
			s.DB,
			models.TableNameTenant,
			models.TableNamePayoutAccount,
			models.TableNameUser,
			models.TableNameOrder,
			models.TableNameTenantLedger,
		)

		u := &models.User{Username: "creator6", Phone: "13700000006", Balance: 5000, IsRealNameVerified: true} // 50.00
		models.UserQuery.WithContext(ctx).Create(u)
		ctx = context.WithValue(ctx, consts.CtxKeyUser, u.ID)

		t := &models.Tenant{UserID: u.ID, Name: "Channel 6", Code: "127", Status: consts.TenantStatusVerified}
		models.TenantQuery.WithContext(ctx).Create(t)
		tenantID = t.ID

		pa := &models.PayoutAccount{
			TenantID: t.ID,
			UserID:   u.ID,
			Type:     "bank",
			Name:     "Bank",
			Account:  "123",
			Realname: "Creator",
		}
		models.PayoutAccountQuery.WithContext(ctx).Create(pa)

		Convey("should withdraw successfully", func() {
			form := &creator_dto.WithdrawForm{
				Amount:    20.00,
				AccountID: pa.ID,
			}
			err := Creator.Withdraw(ctx, tenantID, u.ID, form)
			So(err, ShouldBeNil)

			// Verify Balance Deducted
			uReload, _ := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(u.ID)).First()
			So(uReload.Balance, ShouldEqual, 3000)

			// Verify Order Created
			o, _ := models.OrderQuery.WithContext(ctx).
				Where(models.OrderQuery.TenantID.Eq(t.ID), models.OrderQuery.Type.Eq(consts.OrderTypeWithdrawal)).
				First()
			So(o, ShouldNotBeNil)
			So(o.AmountPaid, ShouldEqual, 2000)

			// Verify Ledger
			l, _ := models.TenantLedgerQuery.WithContext(ctx).Where(models.TenantLedgerQuery.OrderID.Eq(o.ID)).First()
			So(l, ShouldNotBeNil)
			So(l.Type, ShouldEqual, consts.TenantLedgerTypeCreditWithdrawal)
		})

		Convey("should fail if insufficient balance", func() {
			form := &creator_dto.WithdrawForm{
				Amount:    100.00,
				AccountID: pa.ID,
			}
			err := Creator.Withdraw(ctx, tenantID, u.ID, form)
			So(err, ShouldNotBeNil)
		})
	})
}

func (s *CreatorTestSuite) Test_Refund() {
	Convey("Refund", s.T(), func() {
		ctx := s.T().Context()
		tenantID := int64(0)
		database.Truncate(ctx, s.DB,
			models.TableNameTenant, models.TableNameUser, models.TableNameOrder,
			models.TableNameOrderItem, models.TableNameContentAccess, models.TableNameTenantLedger,
		)

		// Creator
		creator := &models.User{Username: "creator7", Phone: "13700000007", Balance: 5000} // Has funds
		models.UserQuery.WithContext(ctx).Create(creator)
		// creatorCtx := context.WithValue(ctx, consts.CtxKeyUser, creator.ID)

		// Tenant
		t := &models.Tenant{UserID: creator.ID, Name: "Channel 7", Code: "128", Status: consts.TenantStatusVerified}
		models.TenantQuery.WithContext(ctx).Create(t)
		tenantID = t.ID

		// Buyer
		buyer := &models.User{Username: "buyer7", Phone: "13900000007", Balance: 0}
		models.UserQuery.WithContext(ctx).Create(buyer)

		// Order (Paid -> Refunding)
		o := &models.Order{
			TenantID:   t.ID,
			UserID:     buyer.ID,
			AmountPaid: 1000, // 10.00
			Status:     consts.OrderStatusRefunding,
		}
		models.OrderQuery.WithContext(ctx).Create(o)
		models.OrderItemQuery.WithContext(ctx).Create(&models.OrderItem{OrderID: o.ID, ContentID: 100}) // Fake content
		models.ContentAccessQuery.WithContext(ctx).
			Create(&models.ContentAccess{UserID: buyer.ID, ContentID: 100, Status: consts.ContentAccessStatusActive})

		Convey("should accept refund", func() {
			form := &creator_dto.RefundForm{Action: "accept", Reason: "Defective"}
			err := Creator.ProcessRefund(ctx, tenantID, creator.ID, o.ID, form)
			So(err, ShouldBeNil)

			// Verify Order
			oReload, _ := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(o.ID)).First()
			So(oReload.Status, ShouldEqual, consts.OrderStatusRefunded)

			// Verify Balances
			cReload, _ := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(creator.ID)).First()
			So(cReload.Balance, ShouldEqual, 4000) // 5000 - 1000

			bReload, _ := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(buyer.ID)).First()
			So(bReload.Balance, ShouldEqual, 1000) // 0 + 1000

			// Verify Access
			acc, _ := models.ContentAccessQuery.WithContext(ctx).
				Where(models.ContentAccessQuery.UserID.Eq(buyer.ID)).
				First()
			So(acc.Status, ShouldEqual, consts.ContentAccessStatusRevoked)
		})
	})
}

func (s *CreatorTestSuite) Test_ReportOverview() {
	Convey("ReportOverview", s.T(), func() {
		ctx := s.T().Context()
		database.Truncate(ctx, s.DB,
			models.TableNameTenant,
			models.TableNameUser,
			models.TableNameContent,
			models.TableNameOrder,
		)

		owner := &models.User{Username: "owner_r", Phone: "13900001011"}
		models.UserQuery.WithContext(ctx).Create(owner)
		tenant := &models.Tenant{
			Name:   "Tenant Report",
			UserID: owner.ID,
			Status: consts.TenantStatusVerified,
		}
		models.TenantQuery.WithContext(ctx).Create(tenant)

		models.ContentQuery.WithContext(ctx).Create(&models.Content{
			TenantID: tenant.ID,
			UserID:   owner.ID,
			Title:    "Content A",
			Status:   consts.ContentStatusPublished,
			Views:    100,
		})

		now := time.Now()
		inRangePaidAt := now.Add(-12 * time.Hour)
		outRangePaidAt := now.Add(-10 * 24 * time.Hour)

		models.OrderQuery.WithContext(ctx).Create(
			&models.Order{
				TenantID:   tenant.ID,
				UserID:     owner.ID,
				Type:       consts.OrderTypeContentPurchase,
				Status:     consts.OrderStatusPaid,
				AmountPaid: 1000,
				PaidAt:     inRangePaidAt,
			},
			&models.Order{
				TenantID:   tenant.ID,
				UserID:     owner.ID,
				Type:       consts.OrderTypeContentPurchase,
				Status:     consts.OrderStatusPaid,
				AmountPaid: 2000,
				PaidAt:     outRangePaidAt,
			},
			&models.Order{
				TenantID:   tenant.ID,
				UserID:     owner.ID,
				Type:       consts.OrderTypeContentPurchase,
				Status:     consts.OrderStatusRefunded,
				AmountPaid: 500,
				UpdatedAt:  now.Add(-6 * time.Hour),
			},
		)

		start := now.Add(-24 * time.Hour).Format(time.RFC3339)
		end := now.Format(time.RFC3339)
		report, err := Creator.ReportOverview(ctx, tenant.ID, owner.ID, &creator_dto.ReportOverviewFilter{
			StartAt: &start,
			EndAt:   &end,
		})
		So(err, ShouldBeNil)
		So(report.Summary.TotalViews, ShouldEqual, 100)
		So(report.Summary.PaidOrders, ShouldEqual, 1)
		So(report.Summary.PaidAmount, ShouldEqual, 10.0)
		So(report.Summary.RefundOrders, ShouldEqual, 1)
		So(report.Summary.RefundAmount, ShouldEqual, 5.0)

		var paidSum, refundSum int64
		for _, item := range report.Items {
			paidSum += item.PaidOrders
			refundSum += item.RefundOrders
		}
		So(paidSum, ShouldEqual, report.Summary.PaidOrders)
		So(refundSum, ShouldEqual, report.Summary.RefundOrders)
	})
}

func (s *CreatorTestSuite) Test_ExportReport() {
	Convey("ExportReport", s.T(), func() {
		ctx := s.T().Context()
		database.Truncate(ctx, s.DB,
			models.TableNameTenant,
			models.TableNameUser,
			models.TableNameContent,
			models.TableNameOrder,
		)

		owner := &models.User{Username: "owner_e", Phone: "13900001012"}
		models.UserQuery.WithContext(ctx).Create(owner)
		tenant := &models.Tenant{
			Name:   "Tenant Export",
			UserID: owner.ID,
			Status: consts.TenantStatusVerified,
		}
		models.TenantQuery.WithContext(ctx).Create(tenant)

		models.ContentQuery.WithContext(ctx).Create(&models.Content{
			TenantID: tenant.ID,
			UserID:   owner.ID,
			Title:    "Content Export",
			Status:   consts.ContentStatusPublished,
			Views:    10,
		})

		models.OrderQuery.WithContext(ctx).Create(&models.Order{
			TenantID:   tenant.ID,
			UserID:     owner.ID,
			Type:       consts.OrderTypeContentPurchase,
			Status:     consts.OrderStatusPaid,
			AmountPaid: 1200,
			PaidAt:     time.Now().Add(-2 * time.Hour),
		})

		form := &creator_dto.ReportExportForm{Format: "csv"}
		resp, err := Creator.ExportReport(ctx, tenant.ID, owner.ID, form)
		So(err, ShouldBeNil)
		So(resp.Filename, ShouldNotBeBlank)
		So(resp.Content, ShouldContainSubstring, "date,paid_orders,paid_amount,refund_orders,refund_amount")
	})
}
