//go:build integration
// +build integration

package integration

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

	"github.com/jackc/pgx/v5/pgxpool"
	_ "github.com/jackc/pgx/v5/stdlib"
	"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"

	repo "route256/comments/infra/repository/sqlc"
	"route256/comments/internal/app/server"
	"route256/comments/internal/domain/service"
	pb "route256/pkg/api/comments/v1"
)

const (
	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": "comments_test",
		},
		ExposedPorts: []string{"5432/tcp"},
		WaitingFor:   wait.ForListeningPort("5432/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.PortEndpoint(ctx, "5432", "")
	if err != nil {
		container.Terminate(ctx)
		return nil, nil, err
	}

	dsn := fmt.Sprintf("postgresql://user:postgres@%s/comments_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 Suite struct {
	suite.Suite

	grpcSrv  *grpc.Server
	grpcConn *grpc.ClientConn

	client pb.CommentsClient

	cleanup func()
}

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

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

		s.cleanup = cleanup

		repo := repo.NewCommentsRepository(pool, pool)

		svc := service.NewCommentsService(repo, 1)

		commentsServer := server.NewServer(svc)

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

		s.grpcSrv = grpc.NewServer()
		pb.RegisterCommentsServer(s.grpcSrv, commentsServer)

		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.client = pb.NewCommentsClient(conn)
	})
}

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

func (s *Suite) TestCommentListByOneUser_Success(t provider.T) {
	t.Title("Успешное получение списка комментариев, которые оставил один юзер по SKU")

	ctx := context.Background()

	const (
		userID       = int64(40)
		sku          = int64(40)
		commentText1 = "тестовый комментарий 1"
		commentText2 = "тестовый комментарий 2"
	)

	var (
		commentIDs []int64
		commentID  int64
	)

	t.WithNewParameters("userID", userID, "sku", sku)

	t.WithNewStep("Создание комментария 1", func(sCtx provider.StepCtx) {
		req := &pb.CreateCommentRequest{
			UserId:  userID,
			Sku:     sku,
			Comment: commentText1,
		}
		resp, err := s.client.CommentAdd(ctx, req)
		sCtx.Require().NoError(err)
		commentID = resp.Id

		commentIDs = append(commentIDs, commentID)
	})

	t.WithNewParameters("commentID1", commentID)

	t.WithNewStep("Получение первого созданного комментария", func(sCtx provider.StepCtx) {
		req := &pb.ListByUserRequest{
			UserId: userID,
		}
		resp, err := s.client.CommentListByUser(ctx, req)
		sCtx.Require().NoError(err)

		sCtx.Require().Len(resp.Comments, 1)
		sCtx.Require().NotNil(resp.Comments[0])

		resultComment := resp.Comments[0]

		sCtx.Require().Equal(commentID, resultComment.Id)
		sCtx.Require().Equal(sku, resultComment.Sku)
		sCtx.Require().Equal(commentText1, resultComment.Text)
	})

	t.WithNewStep("Создание комментария 2", func(sCtx provider.StepCtx) {
		req := &pb.CreateCommentRequest{
			UserId:  userID,
			Sku:     sku,
			Comment: commentText2,
		}
		resp, err := s.client.CommentAdd(ctx, req)
		sCtx.Require().NoError(err)

		commentID = resp.Id
		commentIDs = append(commentIDs, commentID)
	})

	t.WithNewParameters("commentID2", commentID)

	t.WithNewStep("Получение созданных комментариев в обратном хронологическом порядке", func(sCtx provider.StepCtx) {
		req := &pb.ListByUserRequest{
			UserId: userID,
		}
		resp, err := s.client.CommentListByUser(ctx, req)
		sCtx.Require().NoError(err)

		sCtx.Require().Len(resp.Comments, 2)
		sCtx.Require().NotNil(resp.Comments[0])
		sCtx.Require().NotNil(resp.Comments[1])

		// commentIDs содержит в себе id комментариев в прямом хронологическом порядке
		sCtx.Require().Equal(commentIDs[1], resp.Comments[0].Id)
		sCtx.Require().Equal(sku, resp.Comments[0].Sku)
		sCtx.Require().Equal(commentText2, resp.Comments[0].Text)

		sCtx.Require().Equal(commentIDs[0], resp.Comments[1].Id)
		sCtx.Require().Equal(sku, resp.Comments[1].Sku)
		sCtx.Require().Equal(commentText1, resp.Comments[1].Text)
	})
}
