package catalogpublication

import (
	"context"
	"database/sql"
	"errors"
	"strconv"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
)

func TestAdoptStoppedWriterStateSelectsAlreadyCommittedInitialLineage(t *testing.T) {
	authority, mock, closeDB := mockPublicationAuthority(t)
	defer closeDB()
	request := stoppedAdoptionRequest()
	beginRequest := stoppedAdoptionBeginRequest(request, Reference{})
	candidateDigest, requestDigest, err := validateBeginRequest(beginRequest)
	if err != nil {
		t.Fatal(err)
	}
	wantedOutcome := Outcome{
		OrgID:                      request.Candidate.OrgID,
		CommandID:                  request.CommandID,
		CandidatePublicationDigest: candidateDigest,
		Result:                     ResultSelected,
		ResultingActive:            Reference{Present: true, Digest: candidateDigest},
	}
	wantedOutcome.OutcomeDigest, err = canonicalOutcomeDigest(wantedOutcome)
	if err != nil {
		t.Fatal(err)
	}

	mock.ExpectBegin()
	mock.ExpectQuery("SELECT OrgId, CommandId, RequestDigest").
		WithArgs(request.Candidate.OrgID, request.CommandID).
		WillReturnError(sql.ErrNoRows)
	expectNoCandidateSourceJournal(mock, request.Candidate.OrgID, request.CommandID)
	mock.ExpectQuery("SELECT OrgId, RuntimeAudience, CatalogRevision, LastSecurityEpoch, ActiveOperationId").
		WithArgs(request.Candidate.OrgID).
		WillReturnRows(sqlmock.NewRows([]string{
			"OrgId", "RuntimeAudience", "CatalogRevision", "LastSecurityEpoch",
			"ActiveOperationId", "WriterGeneration",
		}).AddRow(request.Candidate.OrgID, request.RuntimeAudience, []byte("7"), []byte("11"), nil, []byte("4")))
	mock.ExpectQuery("SELECT OrgId, CommandId, RequestDigest").
		WithArgs(request.Candidate.OrgID, request.CommandID).
		WillReturnError(sql.ErrNoRows)
	expectNoCandidateSourceJournal(mock, request.Candidate.OrgID, request.CommandID)
	mock.ExpectQuery("SELECT OrgId, ActivePublicationPresent").
		WithArgs(request.Candidate.OrgID).
		WillReturnError(sql.ErrNoRows)
	mock.ExpectExec("INSERT INTO _System_CatalogPublicationActive").
		WithArgs(request.Candidate.OrgID).
		WillReturnResult(sqlmock.NewResult(0, 1))
	expectPublicationInsert(mock, Publication{Digest: candidateDigest, Binding: request.Candidate}).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec("INSERT INTO _System_CatalogPublicationCommand").
		WithArgs(request.Candidate.OrgID, request.CommandID, requestDigest, request.RuntimeAudience,
			candidateDigest, false, nil).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec("UPDATE _System_CatalogPublicationActive").
		WithArgs(candidateDigest, request.CommandID, "1", request.Candidate.OrgID, "0").
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec("INSERT INTO _System_CatalogPublicationOutcome").
		WithArgs(request.Candidate.OrgID, request.CommandID, candidateDigest, string(ResultSelected),
			false, nil, true, candidateDigest, wantedOutcome.OutcomeDigest).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec("INSERT INTO _System_CatalogPublicationHistory").
		WithArgs(request.Candidate.OrgID, "1", request.CommandID, candidateDigest, string(ResultSelected),
			false, nil, true, candidateDigest, wantedOutcome.OutcomeDigest).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec("UPDATE _System_CatalogPublicationCommand").
		WithArgs(request.Candidate.OrgID, request.CommandID, candidateDigest).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectCommit()

	outcome, err := authority.AdoptStoppedWriterState(context.Background(), request)
	if err != nil {
		t.Fatal(err)
	}
	if outcome != wantedOutcome {
		t.Fatalf("outcome = %#v, want %#v", outcome, wantedOutcome)
	}
	assertMockExpectations(t, mock)
}

func TestAdoptStoppedWriterStateExactRetryDoesNotInspectCurrentState(t *testing.T) {
	authority, mock, closeDB := mockPublicationAuthority(t)
	defer closeDB()
	request := stoppedAdoptionRequest()
	beginRequest := stoppedAdoptionBeginRequest(request, Reference{})
	candidateDigest, requestDigest, err := validateBeginRequest(beginRequest)
	if err != nil {
		t.Fatal(err)
	}
	command := commandFromRequest(beginRequest, candidateDigest, requestDigest)
	command.State = CommandCompleted
	outcome := Outcome{
		OrgID:                      request.Candidate.OrgID,
		CommandID:                  request.CommandID,
		CandidatePublicationDigest: candidateDigest,
		Result:                     ResultSelected,
		ResultingActive:            Reference{Present: true, Digest: candidateDigest},
	}
	outcome.OutcomeDigest, err = canonicalOutcomeDigest(outcome)
	if err != nil {
		t.Fatal(err)
	}

	mock.ExpectBegin()
	expectCommandRow(mock, command)
	expectNoCandidateSourceJournal(mock, request.Candidate.OrgID, request.CommandID)
	mock.ExpectQuery("SELECT OrgId, CommandId, CandidatePublicationDigest, Result").
		WithArgs(request.Candidate.OrgID, request.CommandID).
		WillReturnRows(sqlmock.NewRows([]string{
			"OrgId", "CommandId", "CandidatePublicationDigest", "Result", "ObservedPriorPresent",
			"ObservedPriorDigest", "ResultingActivePresent", "ResultingActiveDigest", "OutcomeDigest",
		}).AddRow(outcome.OrgID, outcome.CommandID, outcome.CandidatePublicationDigest, string(outcome.Result),
			false, nil, true, candidateDigest, outcome.OutcomeDigest))
	mock.ExpectCommit()

	recovered, err := authority.AdoptStoppedWriterState(context.Background(), request)
	if err != nil {
		t.Fatal(err)
	}
	if recovered != outcome {
		t.Fatalf("recovered = %#v, want %#v", recovered, outcome)
	}
	assertMockExpectations(t, mock)
}

func TestAdoptStoppedWriterStateRejectsActiveWriterAndMixedRevision(t *testing.T) {
	for name, test := range map[string]struct {
		revision uint64
		command  any
	}{
		"mixed revision": {revision: 6, command: nil},
		"active writer":  {revision: 7, command: "01900000-0000-7000-8000-000000000088"},
	} {
		t.Run(name, func(t *testing.T) {
			authority, mock, closeDB := mockPublicationAuthority(t)
			defer closeDB()
			request := stoppedAdoptionRequest()
			mock.ExpectBegin()
			mock.ExpectQuery("SELECT OrgId, CommandId, RequestDigest").
				WithArgs(request.Candidate.OrgID, request.CommandID).
				WillReturnError(sql.ErrNoRows)
			expectNoCandidateSourceJournal(mock, request.Candidate.OrgID, request.CommandID)
			mock.ExpectQuery("SELECT OrgId, RuntimeAudience, CatalogRevision, LastSecurityEpoch, ActiveOperationId").
				WithArgs(request.Candidate.OrgID).
				WillReturnRows(sqlmock.NewRows([]string{
					"OrgId", "RuntimeAudience", "CatalogRevision", "LastSecurityEpoch",
					"ActiveOperationId", "WriterGeneration",
				}).AddRow(request.Candidate.OrgID, request.RuntimeAudience,
					[]byte(strconv.FormatUint(test.revision, 10)),
					[]byte("11"), test.command, []byte("4")))
			mock.ExpectQuery("SELECT OrgId, CommandId, RequestDigest").
				WithArgs(request.Candidate.OrgID, request.CommandID).
				WillReturnError(sql.ErrNoRows)
			expectNoCandidateSourceJournal(mock, request.Candidate.OrgID, request.CommandID)
			if test.command == nil {
				mock.ExpectQuery("SELECT OrgId, ActivePublicationPresent").
					WithArgs(request.Candidate.OrgID).
					WillReturnError(sql.ErrNoRows)
				mock.ExpectExec("INSERT INTO _System_CatalogPublicationActive").
					WithArgs(request.Candidate.OrgID).
					WillReturnResult(sqlmock.NewResult(0, 1))
			}
			mock.ExpectRollback()

			if _, err := authority.AdoptStoppedWriterState(context.Background(), request); !errors.Is(err, ErrConflict) {
				t.Fatalf("error = %v", err)
			}
			assertMockExpectations(t, mock)
		})
	}
}

func stoppedAdoptionRequest() StoppedAdoptionRequest {
	request := validBeginRequest()
	request.CommandID = "01900000-0000-7000-8000-000000000099"
	request.Candidate.CatalogRevision = 7
	request.Candidate.SchemaGeneration = 2
	request.Candidate.EngineGeneration = 3
	request.Candidate.SecurityEpoch = 11
	return StoppedAdoptionRequest{
		CommandID:       request.CommandID,
		RuntimeAudience: request.RuntimeAudience,
		Candidate:       request.Candidate,
	}
}
