//go:build integration
// +build integration

package integration

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

	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/ozontech/allure-go/pkg/framework/provider"
	"github.com/ozontech/allure-go/pkg/framework/suite"
	"github.com/pressly/goose/v3"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

	"route256/loms/internal/app/server"
	"route256/loms/internal/domain/entity"
	ordersRepository "route256/loms/internal/domain/repository/orders/sqlc"
	stocksRepository "route256/loms/internal/domain/repository/stocks/sqlc"
	lomsService "route256/loms/internal/domain/service"
	"route256/loms/internal/infra/postgres"

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

const (
	// "total_count": 100,
	// "reserved": 35
	testSKU = entity.Sku(1076963)

	testUID       = entity.ID(1337)
	testCount     = uint32(2)
	migrationsDir = "../../db/migrations"
)

func startPostgres(ctx context.Context, migrationsDir string) (*pgxpool.Pool, func(), error) {
	req := testcontainers.ContainerRequest{
		Image: "gitlab-registry.ozon.dev/go/classroom-18/students/base/postgres:16",
		Env: map[string]string{
			"POSTGRESQL_USERNAME": "user",
			"POSTGRESQL_PASSWORD": "postgres",
			"POSTGRESQL_DATABASE": "loms_test",
			"POSTGRESQL_PORT":     "5437",
		},
		ExposedPorts: []string{"5437/tcp"},
		WaitingFor:   wait.ForListeningPort("5437/tcp").WithStartupTimeout(30 * time.Second),
	}
	container, err := testcontainers.GenericContainer(ctx,
		testcontainers.GenericContainerRequest{ContainerRequest: req, Started: true})
	if err != nil {
		return nil, nil, err
	}

	endpoint, err := container.Endpoint(ctx, "")
	if err != nil {
		container.Terminate(ctx)
		return nil, nil, err
	}

	dsn := fmt.Sprintf("postgresql://user:postgres@%s/loms_test?sslmode=disable", endpoint)

	var pool *pgxpool.Pool
	for i := 0; i < 30; i++ {
		pool, err = pgxpool.New(ctx, dsn)
		if err == nil && pool.Ping(ctx) == nil {
			break
		}
		time.Sleep(1 * time.Second)
	}
	if err != nil {
		container.Terminate(ctx)
		return nil, nil, err
	}

	std, err := sql.Open("pgx", dsn)
	if err != nil {
		container.Terminate(ctx)
		return nil, nil, err
	}
	if err := goose.Up(std, migrationsDir); err != nil {
		container.Terminate(ctx)
		return nil, nil, err
	}
	std.Close()

	cleanup := func() {
		pool.Close()
		_ = container.Terminate(context.Background())
	}
	return pool, cleanup, nil
}

type LomsIntegrationSuite struct {
	suite.Suite

	grpcSrv  *grpc.Server
	grpcConn *grpc.ClientConn

	lomsClient pb.LOMSClient

	cleanup func()
}

func TestLomsIntegrationSuite(t *testing.T) {
	suite.RunSuite(t, new(LomsIntegrationSuite))
}

func (s *LomsIntegrationSuite) BeforeAll(t provider.T) {
	ctx := context.Background()
	t.WithNewStep("init cart-service", func(sCtx provider.StepCtx) {
		pool, cleanup, err := startPostgres(ctx, migrationsDir)
		s.cleanup = cleanup

		orderRepo := ordersRepository.NewOrderRepository(pool)
		stockRepo := stocksRepository.NewStockRepository(pool, pool)

		txManager := postgres.NewTxManager(pool, pool)

		svc := lomsService.NewLomsService(orderRepo, stockRepo, txManager)
		lomsServer := server.NewServer(svc)

		lis, err := net.Listen("tcp", "127.0.0.1:0")
		sCtx.Require().NoError(err)

		s.grpcSrv = grpc.NewServer()
		pb.RegisterLOMSServer(s.grpcSrv, lomsServer)

		go func() { _ = s.grpcSrv.Serve(lis) }()

		time.Sleep(50 * time.Millisecond)

		conn, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock())
		sCtx.Require().NoError(err)

		s.grpcConn = conn
		s.lomsClient = pb.NewLOMSClient(conn)
	})
}

func (s *LomsIntegrationSuite) AfterAll(t provider.T) {
	s.grpcSrv.Stop()
	_ = s.grpcConn.Close()
	s.cleanup()
}

func (s *LomsIntegrationSuite) TestOrderProcessPositive(t provider.T) {
	ctx := context.Background()

	var orderID int64

	t.WithNewStep("create order", func(sCtx provider.StepCtx) {
		req := &pb.OrderCreateRequest{
			UserId: int64(testUID),
			Items: []*pb.OrderItem{{
				Sku:   int64(testSKU),
				Count: testCount,
			}},
		}

		resp, err := s.lomsClient.OrderCreate(ctx, req)
		sCtx.Require().NoError(err)
		sCtx.Require().Greater(resp.OrderId, int64(0))
		orderID = resp.OrderId
	})

	t.WithNewStep("verify order info (NEW)", func(sCtx provider.StepCtx) {
		resp, err := s.lomsClient.OrderInfo(ctx, &pb.OrderInfoRequest{OrderId: orderID})
		sCtx.Require().NoError(err)

		sCtx.Require().Equal("awaiting payment", resp.Status)
		sCtx.Require().Equal(int64(testUID), resp.UserId)
		sCtx.Require().Len(resp.Items, 1)
		sCtx.Require().Equal(int64(testSKU), resp.Items[0].Sku)
		sCtx.Require().Equal(testCount, resp.Items[0].Count)
	})

	t.WithNewStep("pay order", func(sCtx provider.StepCtx) {
		_, err := s.lomsClient.OrderPay(ctx, &pb.OrderPayRequest{OrderId: orderID})
		sCtx.Require().NoError(err)
	})

	t.WithNewStep("verify order info (PAYED)", func(sCtx provider.StepCtx) {
		resp, err := s.lomsClient.OrderInfo(ctx, &pb.OrderInfoRequest{OrderId: orderID})
		sCtx.Require().NoError(err)
		sCtx.Require().Equal("payed", resp.Status)
	})
}

func (s *LomsIntegrationSuite) TestStocksInfoPositive(t provider.T) {
	ctx := context.Background()

	t.WithNewStep("call StocksInfo", func(sCtx provider.StepCtx) {
		resp, err := s.lomsClient.StocksInfo(ctx, &pb.StocksInfoRequest{Sku: int64(testSKU)})
		sCtx.Require().NoError(err)
		sCtx.Require().Greater(resp.Count, uint32(0))
	})
}
