package tests

import (
	"errors"
	"fmt"
	"testing"
	"time"

	"github.com/pingcap/dbaas/billing2/handler/paymenthandler"

	"github.com/google/uuid"
	"github.com/pingcap/dbaas/billing2/db"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/handler/centralhandler"
	"github.com/pingcap/dbaas/billing2/model/payment/reconciliation"
	"github.com/pingcap/dbaas/billing2/utils/tests"
	"github.com/shopspring/decimal"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/suite"
	"gorm.io/gorm"
)

type ReconciliationSuite struct {
	tests.BillingTestSuite
}

func TestReconciliationSuite(t *testing.T) {
	suite.Run(t, new(ReconciliationSuite))
}

var reconciliationModel = &reconciliation.Model{}

func (s *ReconciliationSuite) TestBuildCSV() {
	assert := s.Assert()
	header := []string{"Tenant ID", "Marketplace Code", "Total Sent Metering", "Revenue"}
	data := [][]string{
		{"1", "x1\"", "22.22", "32.22"},
		{"2", "x2", "11.22", "12.22"},
	}
	raw, err := reconciliationModel.BuildCSV(header, data)
	assert.NoError(err)
	target := `Tenant ID,Marketplace Code,Total Sent Metering,Revenue
1,"x1""",22.22,32.22
2,x2,11.22,12.22
`
	assert.Equal(target, string(raw))
}

type MockCentralHandlerImpl struct {
	centralhandler.CentralHandler
	tenants []forms.Tenant
}

func (*MockCentralHandlerImpl) GetTenant(tid uint) (*forms.Tenant, error) {
	return &forms.Tenant{
		TenantID: tid,
		Name:     fmt.Sprint(tid),
	}, nil
}

func (*MockCentralHandlerImpl) GetProjectInfo(projectID uint) (*forms.Project, error) {
	return &forms.Project{
		ID:   projectID,
		Name: fmt.Sprint(projectID),
	}, nil
}

func mockCentral() {
	central := &MockCentralHandlerImpl{}
	centralhandler.SetCentralHandler(central)
}

func (s *ReconciliationSuite) TestBuildMonthlyReport() {
	mockCentral()
	assert := s.Assert()
	date := time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC)
	thirdParty := "aws"
	report := []*reconciliation.MonthlyCheckResult{
		{
			CheckResult: reconciliation.CheckResult{
				ThirdPartyName:  thirdParty,
				Pass:            true,
				TenantID:        1234,
				ThirdPartyCode:  "x1",
				PaymentMethodID: 3033,
				Revenue:         decimal.RequireFromString("21.11"),
				Invoice:         decimal.RequireFromString("21.11"),
			},
			Bills: decimal.RequireFromString("22.22"),
			RevenueStructure: []*reconciliation.RevenueStructureDetail{
				{
					Type:  reconciliation.RevenueStructureDetailType_Bills,
					Value: decimal.RequireFromString("22.22"),
					Date:  date,
				},
				{
					Type:  reconciliation.RevenueStructureDetailType_Rest,
					Value: decimal.RequireFromString("1.0"),
					Date:  date.AddDate(0, -1, 0),
				},
				{
					Type:  reconciliation.RevenueStructureDetailType_Rest,
					Value: decimal.RequireFromString("-2.1"),
					Date:  date,
				},
			},
		},
		{
			CheckResult: reconciliation.CheckResult{
				ThirdPartyName:  thirdParty,
				Pass:            false,
				TenantID:        1235,
				ThirdPartyCode:  "x2",
				PaymentMethodID: 3034,
				Revenue:         decimal.RequireFromString("0"),
				Invoice:         decimal.RequireFromString("21.11"),
			},
			Bills: decimal.RequireFromString("22.22"),
			RevenueStructure: []*reconciliation.RevenueStructureDetail{
				{
					Type:  reconciliation.RevenueStructureDetailType_Bills,
					Value: decimal.RequireFromString("22.22"),
					Date:  date,
				},
				{
					Type:  reconciliation.RevenueStructureDetailType_Rest,
					Value: decimal.RequireFromString("1.0"),
					Date:  date.AddDate(0, -1, 0),
				},
				{
					Type:  reconciliation.RevenueStructureDetailType_Rest,
					Value: decimal.RequireFromString("-2.1"),
					Date:  date,
				},
				{
					Type:  reconciliation.RevenueStructureDetailType_TechnicalLoss,
					Value: decimal.RequireFromString("-21.11"),
					Date:  date,
				},
			},
		},
	}
	raw, err := reconciliationModel.BuildMonthlyReport(date, thirdParty, report)
	assert.NoError(err)
	target := "Tenant ID,Tenant Name,Marketplace Code,Bills,Total Sent Metering,Revenue,Check Result,Revenue Structure\n" +
		"1234,1234,x1,22.22,21.11,21.11,true,$21.11 (Revenue in 2023-09) = \n" +
		",,,,,,,,+$22.22 (Bills in 2023-09)\n" +
		",,,,,,,,+$1 (Rest in 2023-08)\n" +
		",,,,,,,,-$2.1 (Rest in 2023-09)\n" +
		"1235,1235,x2,22.22,0,21.11,false,$0 (Revenue in 2023-09) = \n" +
		",,,,,,,,+$22.22 (Bills in 2023-09)\n" +
		",,,,,,,,+$1 (Rest in 2023-08)\n" +
		",,,,,,,,-$2.1 (Rest in 2023-09)\n" +
		",,,,,,,,-$21.11 (Technical Loss)\n"
	assert.Equal(target, string(raw))
}

func (s *ReconciliationSuite) TestCheck() {
	assert := s.Assert()
	date := time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC)
	thirdParty := entity.PaymentType_AWS
	payments := []*entity.PaymentMethod{
		{Model: gorm.Model{ID: 109}, TenantID: 1113, PaymentType: entity.PaymentType_AWS, PaymentCode: "1113"},
		{Model: gorm.Model{ID: 111}, TenantID: 1111, PaymentType: entity.PaymentType_AWS, PaymentCode: "1111"},
		{Model: gorm.Model{ID: 122}, TenantID: 1199, PaymentType: entity.PaymentType_AWS, PaymentCode: "3333"},
	}
	for _, ent := range payments {
		err := ent.Create(db.GetDB())
		assert.NoError(err)
	}
	invoiceFunc := func(date time.Time, paymentType string) ([]*reconciliation.TenantReconciliationInvoice, error) {
		return []*reconciliation.TenantReconciliationInvoice{
			{
				TenantID:        1111,
				Invoice:         decimal.RequireFromString("23.11"),
				PaymentMethodID: 111,
				InvoiceDetail:   map[string]decimal.Decimal{},
			},
			{
				TenantID:        1113,
				Invoice:         decimal.RequireFromString("23.11"),
				PaymentMethodID: 109,
				InvoiceDetail:   map[string]decimal.Decimal{},
			},
			{
				TenantID:        1199,
				Invoice:         decimal.RequireFromString("23.11"),
				PaymentMethodID: 122,
				InvoiceDetail:   map[string]decimal.Decimal{},
			},
		}, nil
	}
	revenueFunc := func(date time.Time) ([]*paymenthandler.TenantRevenue, error) {
		return []*paymenthandler.TenantRevenue{
			{
				PaymentCode: "1111",
				Revenue:     decimal.RequireFromString("23.11"),
			},
			{
				PaymentCode: "1112",
				Revenue:     decimal.RequireFromString("22.11"),
			},
			{
				PaymentCode: "1113",
				Revenue:     decimal.RequireFromString("0"),
			},
		}, nil
	}
	results, err := reconciliationModel.Check(date, thirdParty, invoiceFunc, revenueFunc)
	assert.NoError(err)
	targets := map[uint]*reconciliation.CheckResult{
		1111: {
			ThirdPartyName:  thirdParty,
			Pass:            true,
			TenantID:        1111,
			ThirdPartyCode:  "1111",
			PaymentMethodID: 111,
			Revenue:         decimal.RequireFromString("23.11"),
			Invoice:         decimal.RequireFromString("23.11"),
			InvoiceDetail:   map[string]decimal.Decimal{},
		},
		1113: {
			ThirdPartyName:  thirdParty,
			Pass:            false,
			TenantID:        1113,
			ThirdPartyCode:  "1113",
			PaymentMethodID: 109,
			Revenue:         decimal.RequireFromString("0"),
			Invoice:         decimal.RequireFromString("23.11"),
			InvoiceDetail:   map[string]decimal.Decimal{},
		},
		1199: {
			ThirdPartyName:  thirdParty,
			Pass:            false,
			TenantID:        1199,
			ThirdPartyCode:  "3333",
			PaymentMethodID: 122,
			Revenue:         decimal.Zero,
			Invoice:         decimal.RequireFromString("23.11"),
			InvoiceDetail:   map[string]decimal.Decimal{},
		},
		0: {
			ThirdPartyName:  thirdParty,
			Pass:            false,
			TenantID:        0,
			ThirdPartyCode:  "1112",
			PaymentMethodID: 0,
			Revenue:         decimal.RequireFromString("22.11"),
			Invoice:         decimal.Zero,
		},
	}
	assert.Len(results, len(targets))
	for _, r := range results {
		t, ok := targets[r.TenantID]
		assert.True(ok)
		assert.Equal(t, r)
	}
}

func (s *ReconciliationSuite) TestBuildMonthlyCheckResult() {
	assert := s.Assert()
	month := time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC)
	thirdParty := "aws"
	payment := &entity.PaymentMethod{
		Model: gorm.Model{ID: 1}, TenantID: 1, PaymentType: entity.PaymentType_AWS}
	err := payment.Create(db.GetDB())
	payment2 := &entity.PaymentMethod{
		Model: gorm.Model{ID: 2}, TenantID: 1, PaymentType: entity.PaymentType_STRIPE}
	err = payment2.Create(db.GetDB())
	assert.NoError(err)
	invoices := []*entity.Invoice{
		{
			TenantID:        1,
			InvoiceDate:     time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
			SentDate:        &oct,
			Status:          entity.InvoiceStatus_SENT,
			CostPrice:       decimal.RequireFromString("21"),
			PaymentMethodID: &payment.ID,
		},
		{
			TenantID:        1,
			InvoiceDate:     time.Date(2023, 8, 2, 0, 0, 0, 0, time.UTC),
			CostPrice:       decimal.RequireFromString("2.11"),
			SentDate:        &oct,
			Status:          entity.InvoiceStatus_SENT,
			PaymentMethodID: &payment.ID,
		},
		{
			TenantID:    2,
			InvoiceDate: time.Date(2023, 8, 2, 0, 0, 0, 0, time.UTC),
			CostPrice:   decimal.RequireFromString("2.11"),
			SentDate:    &oct,
			Status:      entity.InvoiceStatus_SENT,
		},
		{
			TenantID:        1,
			InvoiceDate:     time.Date(2023, 9, 29, 0, 0, 0, 0, time.UTC),
			CostPrice:       decimal.RequireFromString("1.11"),
			SentDate:        &oct,
			Status:          entity.InvoiceStatus_SENT,
			PaymentMethodID: &payment2.ID,
		},
		{
			TenantID:        1,
			InvoiceDate:     time.Date(2023, 9, 27, 0, 0, 0, 0, time.UTC),
			CostPrice:       decimal.RequireFromString("7.11"),
			SentDate:        &oct,
			Status:          entity.InvoiceStatus_SENT,
			PaymentMethodID: &payment.ID,
		},
	}
	for _, ent := range invoices {
		err := ent.Create(db.GetDB())
		assert.NoError(err)
		unit_cost := entity.UserUnitCost{
			CostID:         uuid.NewString(),
			CostPrice:      ent.CostPrice.Add(decimal.RequireFromString("0.01")),
			RawPrice:       ent.RawPrice,
			TenantID:       ent.TenantID,
			CostTimeStamp:  ent.InvoiceDate.Unix(),
			Status:         entity.UserUnitCostStatus_DONE,
			ConsumedSource: "invoice",
			InvoiceID:      int(ent.ID),
		}
		err = unit_cost.Create(db.GetDB())
		assert.NoError(err)
	}
	assert.NoError(err)
	cr := &reconciliation.CheckResult{
		ThirdPartyName:  thirdParty,
		Pass:            false,
		TenantID:        1,
		ThirdPartyCode:  "1111",
		PaymentMethodID: 1,
		Revenue:         decimal.RequireFromString("22.11"),
		Invoice:         decimal.RequireFromString("23.11"),
		InvoiceDetail:   map[string]decimal.Decimal{"2023-08": decimal.RequireFromString("2.11"), "2023-09": decimal.RequireFromString("21")},
	}
	result, err := reconciliationModel.BuildMonthlyCheckResult(cr, month)
	assert.NoError(err)
	assert.Equal(*cr, result.CheckResult)
	assert.Equal(decimal.RequireFromString("29.25"), result.Bills)
	assert.Len(result.RevenueStructure, 6)
	assert.Equal(decimal.RequireFromString("29.25"), result.RevenueStructure[0].Value)
	assert.Equal(reconciliation.RevenueStructureDetailType_Bills, result.RevenueStructure[0].Type)
	assert.Equal("2023-09", result.RevenueStructure[0].Date.Format("2006-01"))
	assert.Equal(decimal.RequireFromString("2.11"), result.RevenueStructure[1].Value)
	assert.Equal(reconciliation.RevenueStructureDetailType_Rest, result.RevenueStructure[1].Type)
	assert.Equal("2023-08", result.RevenueStructure[1].Date.Format("2006-01"))
	assert.Equal(decimal.RequireFromString("0.03"), result.RevenueStructure[2].Value)
	assert.Equal(reconciliation.RevenueStructureDetailType_RoundingLoss, result.RevenueStructure[2].Type)
	assert.Equal(decimal.RequireFromString("1.11"), result.RevenueStructure[3].Value)
	assert.Equal(reconciliation.RevenueStructureDetailType_OtherPayment, result.RevenueStructure[3].Type)
	assert.Equal(decimal.RequireFromString("-7.11"), result.RevenueStructure[4].Value)
	assert.Equal(reconciliation.RevenueStructureDetailType_Rest, result.RevenueStructure[4].Type)
	assert.Equal(decimal.RequireFromString("-1"), result.RevenueStructure[5].Value)
	assert.Equal(reconciliation.RevenueStructureDetailType_TechnicalLoss, result.RevenueStructure[5].Type)
}

type MockPaymentHandlerImpl struct {
	mockProviders []paymenthandler.PaymentProvider
}

func (m *MockPaymentHandlerImpl) GetProvider(name string) (paymenthandler.PaymentProvider, error) {
	for _, p := range m.mockProviders {
		if p.GetThirdPartyName() == name {
			return p, nil
		}
	}
	return nil, errors.New("not found")
}

func (m *MockPaymentHandlerImpl) AllProviders() []string {
	ret := make([]string, len(m.mockProviders))
	for i, p := range m.mockProviders {
		ret[i] = p.GetThirdPartyName()
	}
	return ret
}

type MockPaymentProvider1 struct {
	mock.Mock
	paymenthandler.PaymentProvider
}

func (m *MockPaymentProvider1) GetThirdPartyName() string {
	return "mock1"
}

func (m *MockPaymentProvider1) EnableReconciliation() bool {
	return false
}

type MockPaymentProvider2 struct {
	mock.Mock
	paymenthandler.PaymentProvider
}

func (m *MockPaymentProvider2) GetThirdPartyName() string {
	return "mock2"
}

func (m *MockPaymentProvider2) EnableReconciliation() bool {
	return true
}

func (s *ReconciliationSuite) TestRunReconciliationAll() {
	paymenthandler.SetPaymentHandler(&MockPaymentHandlerImpl{
		mockProviders: []paymenthandler.PaymentProvider{&MockPaymentProvider1{}, &MockPaymentProvider2{}},
	})
	assert := s.Assert()
	var count int
	err := reconciliationModel.RunReconciliationAll(func(provider paymenthandler.PaymentProvider) error {
		count++
		return nil
	})
	assert.NoError(err)
	assert.Equal(count, 1)
}
