package tests

import (
	"bytes"
	"encoding/csv"
	"encoding/json"
	"fmt"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/pingcap/dbaas/billing2/db"
	"github.com/pingcap/dbaas/billing2/db/entity"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/dbaas/billing2/api"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/model/billing"
	"github.com/pingcap/dbaas/billing2/utils/ginutils"
	"github.com/pingcap/dbaas/billing2/utils/tests"
	"github.com/stretchr/testify/suite"
)

var model = &billing.UnitModel{}

type UnitTestSuite struct {
	tests.BillingTestSuite
	router *gin.Engine
}

func (suite *UnitTestSuite) SetupTest() {
	suite.BillingTestSuite.SetupTest()
	suite.router = gin.New()
	suite.router.Use(ginutils.ErrorMiddleWare)
	api.InitializeUnitRouter(ginutils.MyRoute{Route: suite.router})
}

func TestUnitSuite(t *testing.T) {
	suite.Run(t, new(UnitTestSuite))
}

func (suite *UnitTestSuite) TestGetAllUnits() {
	assert := suite.Assert()
	w := tests.PerformGetRequest(suite.router, "/units")
	assert.Equal(http.StatusOK, w.Code)
	model.CreateUnit(&forms.UnitInfo{
		UnitKey: "TestKey",
		GroupID: 1,
	})
	w = tests.PerformGetRequest(suite.router, "/units")
	assert.Equal(http.StatusOK, w.Code)
	var response []forms.UnitInfo
	err := json.Unmarshal(w.Body.Bytes(), &response)
	assert.Nil(err)
	assert.Equal(len(response), 1)

	model.CreateUnit(&forms.UnitInfo{
		UnitKey: "TestKey-2",
		GroupID: 1,
	})
	w = tests.PerformGetRequest(suite.router, "/units?group_id=1")
	assert.Equal(http.StatusOK, w.Code)
	err = json.Unmarshal(w.Body.Bytes(), &response)
	assert.Nil(err)
	assert.Equal(len(response), 2)

}

func (suite *UnitTestSuite) TestListUnitsWithGroupCategory() {
	group := &entity.SKUGroups{Category: "compute_%", UnitKeyPattern: "test:{provider}:{region}:manual-price"}
	suite.Require().NoError(group.Create(db.GetDB()))
	for _, unit := range []forms.UnitInfo{
		{UnitKey: "both", GroupID: group.ID, Category: group.Category},
		{UnitKey: "group-only", GroupID: group.ID, Category: "other"},
		{UnitKey: "category-only", GroupID: group.ID + 1, Category: group.Category},
		{UnitKey: "test:aws:us-west-2:manual-price", GroupID: group.ID + 1, Category: "other"},
		{UnitKey: "similar-category", Category: "compute-abc"},
	} {
		_, err := model.CreateUnit(&unit)
		suite.Require().NoError(err)
	}

	for _, suffix := range []string{"", "&match_unit_key_pattern=test:{provider}:{region}:manual-price"} {
		w := tests.PerformGetRequest(suite.router, fmt.Sprintf("/units?group_id=%d%s", group.ID, suffix))
		suite.Require().Equal(http.StatusOK, w.Code)
		var response []forms.UnitInfo
		suite.Require().NoError(json.Unmarshal(w.Body.Bytes(), &response))
		keys := make([]string, 0, len(response))
		for _, unit := range response {
			keys = append(keys, unit.UnitKey)
		}
		suite.ElementsMatch([]string{"both", "group-only", "category-only"}, keys)
	}
}

func (suite *UnitTestSuite) TestListUnitsWithEmptyGroupCategory() {
	group := &entity.SKUGroups{UnitKeyPattern: "test:{provider}"}
	suite.Require().NoError(group.Create(db.GetDB()))
	for _, unit := range []forms.UnitInfo{
		{UnitKey: "assigned", GroupID: group.ID},
		{UnitKey: "unassigned", GroupID: group.ID + 1},
	} {
		_, err := model.CreateUnit(&unit)
		suite.Require().NoError(err)
	}
	w := tests.PerformGetRequest(suite.router, fmt.Sprintf("/units?group_id=%d", group.ID))
	suite.Require().Equal(http.StatusOK, w.Code)
	var response []forms.UnitInfo
	suite.Require().NoError(json.Unmarshal(w.Body.Bytes(), &response))
	suite.Require().Len(response, 1)
	suite.Equal("assigned", response[0].UnitKey)
}

func (suite *UnitTestSuite) TestExportUnitsCSVWithGroupCategory() {
	assert := suite.Assert()
	group := &entity.SKUGroups{Category: "compute", UnitKeyPattern: "test:{provider}"}
	suite.Require().NoError(group.Create(db.GetDB()))
	for _, unit := range []forms.UnitInfo{
		{UnitKey: "test:aws:us-west-2:manual-price", GroupID: group.ID, Category: "compute"},
		{UnitKey: "test:gcp:us-east1:manual-price", GroupID: group.ID + 1, Category: "compute"},
		{UnitKey: "other:aws:us-west-2:manual-price", GroupID: group.ID, Category: "other"},
		{UnitKey: "test:excluded:unit:manual-price", GroupID: group.ID + 1, Category: "other"},
	} {
		_, err := model.CreateUnit(&unit)
		assert.NoError(err)
	}

	w := tests.PerformGetRequest(suite.router, fmt.Sprintf("/units/_export?group_id=%d&match_unit_key_pattern=test:{provider}:{region}:manual-price", group.ID))
	assert.Equal(http.StatusOK, w.Code)
	assert.Contains(w.Header().Get("Content-Type"), "text/csv")
	assert.Contains(w.Header().Get("Content-Disposition"), "attachment")

	records, err := csv.NewReader(w.Body).ReadAll()
	assert.NoError(err)
	assert.Len(records, 4)
	unitKeyIndex := csvColumnIndex(records[0], "unit_key")
	assert.NotEqual(-1, unitKeyIndex)

	unitKeys := map[string]bool{}
	for _, record := range records[1:] {
		unitKeys[record[unitKeyIndex]] = true
	}
	assert.True(unitKeys["test:aws:us-west-2:manual-price"])
	assert.True(unitKeys["test:gcp:us-east1:manual-price"])
	assert.True(unitKeys["other:aws:us-west-2:manual-price"])
}

func (suite *UnitTestSuite) TestExportUnitsCSVWithoutFilterReturnsHeaderOnly() {
	assert := suite.Assert()
	_, err := model.CreateUnit(&forms.UnitInfo{UnitKey: "must-not-be-exported"})
	assert.NoError(err)

	w := tests.PerformGetRequest(suite.router, "/units/_export")
	assert.Equal(http.StatusOK, w.Code)
	records, err := csv.NewReader(w.Body).ReadAll()
	assert.NoError(err)
	assert.Len(records, 1)
	assert.Contains(records[0], "unit_key")
}

func (suite *UnitTestSuite) TestExportedUnitsCSVRoundTripImport() {
	assert := suite.Assert()
	_, err := model.CreateUnit(&forms.UnitInfo{
		UnitKey:     "round-trip-unit",
		Description: "before import",
		GroupID:     8,
	})
	assert.NoError(err)

	exported := tests.PerformGetRequest(suite.router, "/units/_export?group_id=8")
	assert.Equal(http.StatusOK, exported.Code)
	records, err := csv.NewReader(exported.Body).ReadAll()
	assert.NoError(err)
	assert.Len(records, 2)
	descriptionIndex := csvColumnIndex(records[0], "description")
	assert.NotEqual(-1, descriptionIndex)
	records[1][descriptionIndex] = "after import"

	var modified bytes.Buffer
	writer := csv.NewWriter(&modified)
	writer.WriteAll(records)
	writer.Flush()
	assert.NoError(writer.Error())

	req := httptest.NewRequest(http.MethodPost, "/units/_import", &modified)
	req.Header.Set("Content-Type", "text/csv")
	w := httptest.NewRecorder()
	suite.router.ServeHTTP(w, req)
	assert.Equal(http.StatusOK, w.Code, w.Body.String())

	var result forms.UnitCSVImportResult
	assert.NoError(json.Unmarshal(w.Body.Bytes(), &result))
	assert.Equal(1, result.ImportedRows)
	unit, err := model.GetUnit("round-trip-unit")
	assert.NoError(err)
	assert.Equal("after import", unit.Description)
}

func (suite *UnitTestSuite) TestImportUnitsCSVFromMultipart() {
	assert := suite.Assert()
	csvContent := strings.Join([]string{
		"unit_key,description,price,group_id,beta_internal,created_at",
		"existing-unit,updated description,1.25,9,1,not-imported",
		"new-unit,new description,2.5,10,0,\\N",
	}, "\n")
	_, err := model.CreateUnit(&forms.UnitInfo{UnitKey: "existing-unit", Description: "old description"})
	assert.NoError(err)

	w := performUnitCSVMultipartRequest(suite.router, csvContent)
	assert.Equal(http.StatusOK, w.Code, w.Body.String())

	existing, err := model.GetUnit("existing-unit")
	assert.NoError(err)
	assert.Equal("updated description", existing.Description)
	assert.Equal("1.25", existing.Price.String())
	assert.Equal(uint(9), existing.GroupID)
	assert.True(existing.BetaInternal)

	created, err := model.GetUnit("new-unit")
	assert.NoError(err)
	assert.Equal("new description", created.Description)
	assert.Equal("2.5", created.Price.String())
	assert.Equal(uint(10), created.GroupID)
}

func (suite *UnitTestSuite) TestImportUnitsCSVRejectsDuplicatesBeforeWriting() {
	assert := suite.Assert()
	csvContent := strings.Join([]string{
		"unit_key,description",
		"duplicate-unit,first",
		"duplicate-unit,second",
	}, "\n")

	w := performUnitCSVMultipartRequest(suite.router, csvContent)
	assert.Equal(http.StatusBadRequest, w.Code, w.Body.String())
	_, err := model.GetUnit("duplicate-unit")
	assert.Error(err)
}

func csvColumnIndex(header []string, column string) int {
	for index, value := range header {
		if value == column {
			return index
		}
	}
	return -1
}

func performUnitCSVMultipartRequest(router http.Handler, content string) *httptest.ResponseRecorder {
	var body bytes.Buffer
	writer := multipart.NewWriter(&body)
	file, err := writer.CreateFormFile("file", "units.csv")
	if err != nil {
		panic(err)
	}
	if _, err := file.Write([]byte(content)); err != nil {
		panic(err)
	}
	if err := writer.Close(); err != nil {
		panic(err)
	}

	req := httptest.NewRequest(http.MethodPost, "/units/_import", &body)
	req.Header.Set("Content-Type", writer.FormDataContentType())
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)
	return w
}

func (suite *UnitTestSuite) TestCreateUnit() {
	assert := suite.Assert()
	unit := &forms.UnitInfo{
		UnitKey: "TestKey",
	}
	w := tests.PerformRequest(suite.router, "POST", "/units", unit)
	assert.Equal(http.StatusOK, w.Code)
	fmt.Println(w.Body)
	_, err := model.GetUnit("TestKey")
	assert.Nil(err)
}

func (suite *UnitTestSuite) TestCreateUnitWithEmptyKey() {
	assert := suite.Assert()
	unit := &forms.UnitInfo{
		UnitKey: "",
	}
	w := tests.PerformRequest(suite.router, "POST", "/units", unit)
	assert.Equal(http.StatusBadRequest, w.Code)
	fmt.Println(w.Body)
}

func (suite *UnitTestSuite) TestDeleteUnit() {
	assert := suite.Assert()
	unit := &forms.UnitInfo{
		UnitKey: "test_key",
	}
	_, err := model.CreateUnit(unit)
	assert.Nil(err)
	w := tests.PerformRequest(suite.router, "DELETE", "/units/test_key", nil)
	assert.Equal(http.StatusOK, w.Code)
	_, err = model.GetUnit("TestKey")
	assert.NotNil(err)
}

func (suite *UnitTestSuite) TestGetUnit() {
	assert := suite.Assert()
	unit := &forms.UnitInfo{
		UnitKey: "test_key",
	}
	model.CreateUnit(unit)
	w := tests.PerformGetRequest(suite.router, "/units/test_key")
	assert.Equal(http.StatusOK, w.Code)
}

func (suite *UnitTestSuite) TestUpdateUnit() {
	assert := suite.Assert()
	unit := &forms.UnitInfo{
		UnitKey:     "test_key",
		Description: "test description",
	}
	model.CreateUnit(unit)
	description := "test description"
	w := tests.PerformRequest(suite.router, "PUT", "/units/test_key", unit)
	assert.Equal(http.StatusOK, w.Code)
	unitResp, err := model.GetUnit("test_key")
	assert.Nil(err)
	assert.Equal(unitResp.Description, description)
}

func (suite *UnitTestSuite) TestBulkUpdateUnit() {
	assert := suite.Assert()
	units := []forms.UnitInfo{
		{
			UnitKey: "test_key",
		},
		{
			UnitKey: "test_key1",
		},
	}
	w := tests.PerformRequest(suite.router, "POST", "/units/_bulk", units)
	assert.Equal(http.StatusOK, w.Code)

	units = []forms.UnitInfo{
		{
			UnitKey: "test_key",
		},
		{
			UnitKey: "",
		},
	}
	w = tests.PerformRequest(suite.router, "POST", "/units/_bulk", units)
	assert.Equal(http.StatusBadRequest, w.Code, w.Body.String())
}

func (suite *UnitTestSuite) TestAddRegionNetWorkUnit() {
	assert := suite.Assert()
	w := tests.PerformRequest(suite.router, "POST", "/units/network/gcp/asia-east2", "")
	assert.Equal(http.StatusOK, w.Code)
	unitResp, err := model.GetUnit("network-traffic:gcp:asia-east2:cross_az")
	assert.Nil(err)
	assert.Equal(unitResp.Category, "Data Transfer Cost - GCP Same Region")
	assert.Equal(unitResp.Region, "asia-east2")
	var units []entity.Unit
	err = entity.NewUnitQuerySet(db.GetDB()).All(&units)
	assert.NoError(err)
	assert.Equal(4, len(units))
}

// func (m *MockCloudProviderHandlerImpl) GetPrice(params map[string]string) (*forms.ProviderPriceRes, error) {
// 	priceA := decimal.NewFromInt(1)
// 	resFromBase := forms.ProviderPriceRes{
// 		SKU:     "base_price",
// 		Version: timeutils.GetNow().String(),
// 		Price:   &priceA,
// 	}
// 	return &resFromBase, nil
// }

// type MockCloudProviderHandlerImpl struct {
// 	cloudproviderhandler.ProviderPriceHandler
// }

// func (suite *UnitTestSuite) TestCreateUnitByGroup() {
// 	cloudproviderhandler.Set(&MockCloudProviderHandlerImpl{})

// 	//	router.HandleWithError("POST", "/units/_create_by_group", api.CreateUnitByGroup)
// 	assert := suite.Assert()
// 	priceFormula := "{provider_price[service_code=AmazonEC2][provider={provider}][region={region}][instanceType={instanceType}]} * 4 +({base_price[price=12]} *5)"

// 	SKUGroup := forms.SKUGroup{
// 		UnitKeyPattern: "node:{provider}:{region}:DEPLOY_TYPE_DEDICATED:{instanceType}",
// 		Name:           "Name test",
// 		Category:       "Category Test",
// 		PriceType:      "Normal",
// 		MeasureUnit:    "Hrs",
// 		PriceFormula:   priceFormula,
// 	}
// 	model := &billing.SKUGroupModel{}
// 	res, err := model.CreateGroup(&SKUGroup)
// 	assert.NoError(err)
// 	groupID := res.GroupID

// 	unitByGroup := forms.UnitByGroup{
// 		GroupID:     groupID,
// 		Provider:    "aws",
// 		Region:      "aws-1",
// 		GroupLabels: map[string]string{"instanceType": "s2"},
// 	}
// 	var response forms.UnitInfo
// 	w := tests.PerformRequest(suite.router, "POST", "/units/_create_by_group", unitByGroup)
// 	assert.Equal(http.StatusOK, w.Code)
// 	err = json.Unmarshal(w.Body.Bytes(), &response)
// 	assert.Nil(err)
// 	assert.Equal(response.Provider, unitByGroup.Provider)
// 	assert.Equal(response.UnitKey, "node:aws:aws-1:DEPLOY_TYPE_DEDICATED:s2")

// 	w = tests.PerformRequest(suite.router, "POST", "/units/_create_by_group/dryrun", unitByGroup)
// 	assert.Equal(http.StatusOK, w.Code)
// 	err = json.Unmarshal(w.Body.Bytes(), &response)
// 	assert.Nil(err)
// 	assert.Equal(response.Provider, unitByGroup.Provider)
// 	assert.Equal(response.UnitKey, "node:aws:aws-1:DEPLOY_TYPE_DEDICATED:s2")

// }
