//go:build integration
// +build integration

package integration

import (
	"context"
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/ozontech/allure-go/pkg/framework/provider"
	"github.com/ozontech/allure-go/pkg/framework/suite"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"
	"go.uber.org/goleak"
	"google.golang.org/grpc"

	"route256/cart/internal/app"
	lomsService "route256/cart/internal/clients/loms"
	productsService "route256/cart/internal/clients/products"
	"route256/cart/internal/domain/entity"
	"route256/cart/internal/domain/model"
	"route256/cart/internal/domain/repository"
	cartService "route256/cart/internal/domain/service"

	pbLoms "route256/pkg/api/loms/v1"
)

const (
	productServiceImage = "gitlab-registry.ozon.dev/go/classroom-18/students/base/products:latest"
	testToken           = "testToken"

	testSku  = 1076963
	testName = "Теория нравственных чувств | Смит Адам"
	testUID  = entity.UID(1337)
)

type lomsServerMock struct {
	pbLoms.UnimplementedLOMSServer
}

func (lomsServerMock) OrderCreate(_ context.Context, _ *pbLoms.OrderCreateRequest) (*pbLoms.OrderCreateResponse, error) {
	return &pbLoms.OrderCreateResponse{OrderId: 1}, nil
}

func (lomsServerMock) StocksInfo(_ context.Context, _ *pbLoms.StocksInfoRequest) (*pbLoms.StocksInfoResponse, error) {
	return &pbLoms.StocksInfoResponse{Count: 1000}, nil
}

type CartHandlerSuite struct {
	suite.Suite

	psContainer testcontainers.Container

	lomsSrv  *grpc.Server
	lomsConn *grpc.ClientConn

	server  *httptest.Server
	cartSvc *cartService.CartService
}

func TestCartHandlerSuite(t *testing.T) {
	defer goleak.VerifyNone(t)

	suite.RunSuite(t, new(CartHandlerSuite))
}

func (s *CartHandlerSuite) BeforeAll(t provider.T) {
	ctx := context.Background()

	t.WithNewStep("start product-service container", func(sCtx provider.StepCtx) {
		req := testcontainers.ContainerRequest{
			Image:        productServiceImage,
			ExposedPorts: []string{"8082/tcp"},
			WaitingFor:   wait.ForHTTP("/docs").WithStartupTimeout(10 * time.Second),
		}
		container, err := testcontainers.GenericContainer(ctx,
			testcontainers.GenericContainerRequest{
				ContainerRequest: req,
				Started:          true,
			})
		sCtx.Require().NoError(err, "create container")
		s.psContainer = container
	})

	var productURL string
	t.WithNewStep("discover product-service URL", func(sCtx provider.StepCtx) {
		endpoint, err := s.psContainer.Endpoint(ctx, "")
		sCtx.Require().NoError(err)
		productURL = endpoint
	})

	t.WithNewStep("start loms grpc server", func(st provider.StepCtx) {
		lis, err := net.Listen("tcp", "127.0.0.1:0")
		st.Require().NoError(err)

		s.lomsSrv = grpc.NewServer()
		pbLoms.RegisterLOMSServer(s.lomsSrv, &lomsServerMock{})

		go s.lomsSrv.Serve(lis)

		conn, err := grpc.Dial(lis.Addr().String(), grpc.WithInsecure(), grpc.WithBlock())
		st.Require().NoError(err)
		s.lomsConn = conn
	})

	t.WithNewStep("init cart-service", func(sCtx provider.StepCtx) {
		prodClient := productsService.NewProductService(
			*http.DefaultClient,
			testToken,
			productURL,
			10,
			10,
			10,
		)

		lomsClient := pbLoms.NewLOMSClient(s.lomsConn)

		repo := repository.NewInMemoryRepository(10)
		s.cartSvc = cartService.NewCartService(repo, prodClient, lomsService.NewLomsService(lomsClient))

		appSrv := &app.App{}

		s.server = httptest.NewServer(appSrv.BootstrapHandlers(s.cartSvc))
	})
}

func (s *CartHandlerSuite) AfterAll(t provider.T) {
	s.server.Close()
	s.lomsSrv.Stop()
	s.lomsConn.Close()

	_ = s.psContainer.Terminate(context.Background())
}

// DELETE /user/<user_id>/cart/<sku_id>
func (s *CartHandlerSuite) TestDeleteItemPositive(t provider.T) {
	ctx := context.Background()

	item := &model.Item{
		Product: &model.Product{
			Sku: testSku,
		},
		Count: 1,
	}

	t.WithNewStep("fill cart in usecase", func(sCtx provider.StepCtx) {
		err := s.cartSvc.AddItem(context.Background(),
			testUID, item)
		sCtx.Require().NoError(err)

		cart, err := s.cartSvc.GetItemsByUserID(ctx, testUID)
		sCtx.Require().NoError(err)

		sCtx.Require().Len(cart.Items, 1, "check cart contains product")
		sCtx.Require().Equal(testUID, cart.UserID, "check user ID equals")
		sCtx.Require().Equal(item.Product.Sku, cart.Items[0].Product.Sku, "check product sku equals")
	})

	t.WithNewStep("call DELETE handler", func(sCtx provider.StepCtx) {
		url := fmt.Sprintf("%s/user/%d/cart/%d", s.server.URL, testUID, testSku)

		req, err := http.NewRequest(http.MethodDelete, url, nil)
		sCtx.Require().NoError(err)

		resp, err := s.server.Client().Do(req)
		sCtx.Require().NoError(err)
		defer resp.Body.Close()

		sCtx.Require().Equal(http.StatusNoContent, resp.StatusCode, "delete handler unexpected status code")
	})

	t.WithNewStep("check cart empty in usecase", func(sCtx provider.StepCtx) {
		_, err := s.cartSvc.GetItemsByUserID(ctx, testUID)
		sCtx.Require().Error(err)
	})
}

// GET /user/<user_id>/cart
func (s *CartHandlerSuite) TestGetCartPositive(t provider.T) {
	ctx := context.Background()

	item := &model.Item{
		Product: &model.Product{
			Sku: testSku,
		},
		Count: 2,
	}

	t.WithNewStep("fill cart in usecase", func(sCtx provider.StepCtx) {
		err := s.cartSvc.AddItem(context.Background(),
			testUID, item)
		sCtx.Require().NoError(err)

		cart, err := s.cartSvc.GetItemsByUserID(ctx, testUID)
		sCtx.Require().NoError(err)

		sCtx.Require().Len(cart.Items, 1, "check cart contains product")
		sCtx.Require().Equal(testUID, cart.UserID, "check user ID equals")
		sCtx.Require().Equal(item.Product.Sku, cart.Items[0].Product.Sku, "check product sku equals")
		sCtx.Require().Equal(item.Count, cart.Items[0].Count, "check product count")
	})

	t.WithNewStep("call GET handler", func(sCtx provider.StepCtx) {
		url := fmt.Sprintf("%s/user/%d/cart", s.server.URL, testUID)

		req, err := http.NewRequest(http.MethodGet, url, nil)
		sCtx.Require().NoError(err)

		resp, err := s.server.Client().Do(req)
		sCtx.Require().NoError(err)
		defer resp.Body.Close()

		sCtx.Require().Equal(http.StatusOK, resp.StatusCode, "check GET status code")
		sCtx.Require().Equal("application/json", resp.Header.Get("Content-Type"), "check GET content type")

		var body struct {
			Items []struct {
				Sku   int64  `json:"sku"`
				Name  string `json:"name"`
				Count uint32 `json:"count"`
				Price uint32 `json:"price"`
			} `json:"items"`
			TotalPrice uint32 `json:"total_price"`
		}
		sCtx.Require().NoError(json.NewDecoder(resp.Body).Decode(&body))

		sCtx.Require().Len(body.Items, 1, "check items len")
		sCtx.Require().Equal(int64(testSku), body.Items[0].Sku, "check sku")
		sCtx.Require().Equal(uint32(2), body.Items[0].Count, "check item count")
		sCtx.Require().Greater(body.TotalPrice, uint32(0), "check price")
	})
}
