package repository_test

import (
	"context"
	"database/sql"
	"fmt"
	"git.sch9.ru/new_gate/ms-tester/internal/models"
	"git.sch9.ru/new_gate/ms-tester/internal/problems/repository"
	"github.com/DATA-DOG/go-sqlmock"
	"github.com/jmoiron/sqlx"
	"github.com/stretchr/testify/assert"
	"testing"
	"time"
)

// setupTestDB creates a mocked sqlx.DB and sqlmock instance for testing.
func setupTestDB(t *testing.T) (*sqlx.DB, sqlmock.Sqlmock) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
	assert.NoError(t, err)
	sqlxDB := sqlx.NewDb(db, "sqlmock")
	return sqlxDB, mock
}

func TestRepository_CreateProblem(t *testing.T) {
	db, mock := setupTestDB(t)
	defer db.Close()

	repo := repository.NewRepository(db)

	t.Run("success", func(t *testing.T) {
		ctx := context.Background()

		problem := models.Problem{
			Id:    1,
			Title: "Test Problem",
		}

		mock.ExpectQuery(repository.CreateProblemQuery).
			WithArgs(problem.Title).
			WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(problem.Id))

		id, err := repo.CreateProblem(ctx, db, problem.Title)
		assert.NoError(t, err)
		assert.Equal(t, problem.Id, id)
	})
}

func TestRepository_GetProblemById(t *testing.T) {
	db, mock := setupTestDB(t)
	defer db.Close()

	repo := repository.NewRepository(db)

	t.Run("success", func(t *testing.T) {
		ctx := context.Background()

		expected := &models.Problem{
			Id:               1,
			Title:            "Test Problem",
			TimeLimit:        1000,
			MemoryLimit:      1024,
			Legend:           "Test Legend",
			InputFormat:      "Test Input Format",
			OutputFormat:     "Test Output Format",
			Notes:            "Test Notes",
			Scoring:          "Test Scoring",
			LegendHtml:       "Test Legend HTML",
			InputFormatHtml:  "Test Input Format HTML",
			OutputFormatHtml: "Test Output Format HTML",
			NotesHtml:        "Test Notes HTML",
			ScoringHtml:      "Test Scoring HTML",
			CreatedAt:        time.Now(),
			UpdatedAt:        time.Now(),
		}

		columns := []string{
			"id",
			"title",
			"time_limit",
			"memory_limit",

			"legend",
			"input_format",
			"output_format",
			"notes",
			"scoring",

			"legend_html",
			"input_format_html",
			"output_format_html",
			"notes_html",
			"scoring_html",

			"created_at",
			"updated_at",
		}

		rows := sqlmock.NewRows(columns).
			AddRow(
				expected.Id,
				expected.Title,
				expected.TimeLimit,
				expected.MemoryLimit,

				expected.Legend,
				expected.InputFormat,
				expected.OutputFormat,
				expected.Notes,
				expected.Scoring,

				expected.LegendHtml,
				expected.InputFormatHtml,
				expected.OutputFormatHtml,
				expected.NotesHtml,
				expected.ScoringHtml,

				expected.CreatedAt,
				expected.UpdatedAt)

		mock.ExpectQuery(repository.GetProblemByIdQuery).WithArgs(expected.Id).WillReturnRows(rows)

		problem, err := repo.GetProblemById(ctx, db, expected.Id)
		assert.NoError(t, err)
		assert.EqualExportedValues(t, expected, problem)
	})

	t.Run("not found", func(t *testing.T) {
		ctx := context.Background()

		id := int32(1)

		mock.ExpectQuery(repository.GetProblemByIdQuery).WithArgs(id).WillReturnError(sql.ErrNoRows)

		_, err := repo.GetProblemById(ctx, db, id)
		assert.Error(t, err)
	})
}

func TestRepository_DeleteProblem(t *testing.T) {
	db, mock := setupTestDB(t)
	defer db.Close()

	repo := repository.NewRepository(db)

	t.Run("success", func(t *testing.T) {
		ctx := context.Background()

		id := int32(1)

		mock.ExpectExec(repository.DeleteProblemQuery).
			WithArgs(id).WillReturnResult(sqlmock.NewResult(0, 1))

		err := repo.DeleteProblem(ctx, db, id)
		assert.NoError(t, err)
	})

	t.Run("not found", func(t *testing.T) {
		ctx := context.Background()
		id := int32(1)

		mock.ExpectExec(repository.DeleteProblemQuery).WithArgs(id).WillReturnError(sql.ErrNoRows)

		err := repo.DeleteProblem(ctx, db, id)
		assert.Error(t, err)
	})
}

func TestRepository_ListProblems(t *testing.T) {
	db, mock := setupTestDB(t)
	defer db.Close()

	repo := repository.NewRepository(db)

	t.Run("success", func(t *testing.T) {
		ctx := context.Background()

		expected := make([]*models.ProblemsListItem, 0)
		for i := 0; i < 10; i++ {
			problem := &models.ProblemsListItem{
				Id:          int32(i + 1),
				Title:       fmt.Sprintf("Test Problem %d", i+1),
				TimeLimit:   1000,
				MemoryLimit: 1024,
				SolvedCount: int32(123 * i),
				CreatedAt:   time.Now(),
				UpdatedAt:   time.Now(),
			}

			expected = append(expected, problem)
		}

		filter := models.ProblemsFilter{
			Page:     1,
			PageSize: 10,
		}

		var totalCount int32 = 10

		columns := []string{
			"id",
			"title",
			"time_limit",
			"memory_limit",
			"solved_count",
			"created_at",
			"updated_at",
		}

		rows := sqlmock.NewRows(columns)
		for _, problem := range expected {
			rows = rows.AddRow(
				problem.Id,
				problem.Title,
				problem.TimeLimit,
				problem.MemoryLimit,
				problem.SolvedCount,
				problem.CreatedAt,
				problem.UpdatedAt,
			)
		}

		mock.ExpectQuery(repository.ListProblemsQuery).WillReturnRows(rows)
		mock.ExpectQuery(repository.CountProblemsQuery).
			WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(totalCount))

		problems, err := repo.ListProblems(ctx, db, filter)
		assert.NoError(t, err)
		assert.Equal(t, expected, problems.Problems)
		assert.Equal(t, models.Pagination{
			Page:  1,
			Total: 1,
		}, problems.Pagination)
	})
}

func TestRepository_UpdateProblem(t *testing.T) {
	db, mock := setupTestDB(t)
	defer db.Close()

	repo := repository.NewRepository(db)

	t.Run("success", func(t *testing.T) {
		ctx := context.Background()
		var id int32 = 1

		update := &models.ProblemUpdate{
			Title:            sp("Test Problem"),
			TimeLimit:        ip(1000),
			MemoryLimit:      ip(1024),
			Legend:           sp("Test Legend"),
			InputFormat:      sp("Test Input Format"),
			OutputFormat:     sp("Test Output Format"),
			Notes:            sp("Test Notes"),
			Scoring:          sp("Test Scoring"),
			LegendHtml:       sp("Test Legend HTML"),
			InputFormatHtml:  sp("Test Input Format HTML"),
			OutputFormatHtml: sp("Test Output Format HTML"),
			NotesHtml:        sp("Test Notes HTML"),
			ScoringHtml:      sp("Test Scoring HTML"),
		}

		mock.ExpectExec(repository.UpdateProblemQuery).WithArgs(
			id,

			update.Title,
			update.TimeLimit,
			update.MemoryLimit,

			update.Legend,
			update.InputFormat,
			update.OutputFormat,
			update.Notes,
			update.Scoring,

			update.LegendHtml,
			update.InputFormatHtml,
			update.OutputFormatHtml,
			update.NotesHtml,
			update.ScoringHtml,
		).WillReturnResult(sqlmock.NewResult(1, 1))

		err := repo.UpdateProblem(ctx, db, id, update)
		assert.NoError(t, err)
	})
}

func sp(s string) *string {
	return &s
}

func ip(s int32) *int32 {
	return &s
}