package knowledgeqa

import (
	"crypto/sha256"
	"encoding/base64"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
)

func TestCreateSharePersistsAccountBoundEvidenceMappingWithoutOwnerIDInAnswer(t *testing.T) {
	sqlDB, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer sqlDB.Close()
	now := time.Date(2026, 8, 14, 10, 0, 0, 0, time.UTC)

	mock.ExpectBegin()
	mock.ExpectQuery(`(?s)FROM knowledge_qa_turns AS turn.*turn.status='completed'.*FOR UPDATE`).
		WithArgs("wsp_1", "usr_owner", "kqs_1", "kqt_1").
		WillReturnRows(sqlmock.NewRows([]string{
			"organization_id", "session_id", "question", "content", "owner_display_name",
		}).AddRow(
			"org_1", "kqs_1",
			"What changed? [E9](agentteam-kqa-citation://kqs_1/kqc_questionauthority).",
			"Use the new flow [E1](agentteam-kqa-citation://kqs_1/kqc_abcdefghijklmn).",
			"",
		))
	expectShareCitationLoad(mock, now)
	mock.ExpectQuery(`(?s)SELECT id, created_at.*FROM knowledge_qa_shares.*source_turn_id=.*FOR UPDATE`).
		WithArgs("kqt_1").
		WillReturnRows(sqlmock.NewRows([]string{"id", "created_at"}))
	mock.ExpectExec(`(?s)INSERT INTO knowledge_qa_shares`).
		WithArgs(
			sqlmock.AnyArg(), sqlmock.AnyArg(), "wsp_1", "org_1", "kqs_1",
			"kqt_1", "usr_owner", "", "What changed? [E9].",
			"Use the new flow [E1](agentteam-kqa-shared-evidence://E1).", now, now,
		).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec(`DELETE FROM knowledge_qa_share_citations`).
		WithArgs(sqlmock.AnyArg()).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec(`(?s)INSERT INTO knowledge_qa_share_citations`).
		WithArgs(
			sqlmock.AnyArg(), "E1", "kb_1", "src_1", "primary", "primary",
			"sha-historical", "", "", "docs/design.md", int64(4), int64(5), now,
		).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectCommit()

	share, err := New(coredb.WrapSQLDB(sqlDB)).CreateShare(
		t.Context(), "wsp_1", "usr_owner", "kqs_1", "kqt_1", now,
	)
	require.NoError(t, err)
	require.Regexp(t, `^kqh_[a-z0-9]{14}$`, share.ID)
	require.Regexp(t, `^kqshare_[A-Za-z0-9_-]{43}$`, share.Token)
	require.Empty(t, share.OwnerDisplayName)
	require.Equal(t, "What changed? [E9].", share.Question)
	require.Equal(t, "Use the new flow [E1](agentteam-kqa-shared-evidence://E1).", share.Answer)
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestCreateShareRotatesExistingInvitationWithoutReplacingRecipients(t *testing.T) {
	sqlDB, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer sqlDB.Close()
	now := time.Date(2026, 8, 14, 11, 0, 0, 0, time.UTC)
	createdAt := now.Add(-time.Hour)

	mock.ExpectBegin()
	mock.ExpectQuery(`(?s)FROM knowledge_qa_turns AS turn.*turn.status='completed'.*FOR UPDATE`).
		WithArgs("wsp_1", "usr_owner", "kqs_1", "kqt_1").
		WillReturnRows(sqlmock.NewRows([]string{
			"organization_id", "session_id", "question", "content", "owner_display_name",
		}).AddRow("org_1", "kqs_1", "Question?", "Answer [E1](agentteam-kqa-citation://kqs_1/kqc_abcdefghijklmn).", "Owner"))
	expectShareCitationLoad(mock, now)
	mock.ExpectQuery(`(?s)SELECT id, created_at.*FROM knowledge_qa_shares.*source_turn_id=.*FOR UPDATE`).
		WithArgs("kqt_1").
		WillReturnRows(sqlmock.NewRows([]string{"id", "created_at"}).
			AddRow("kqh_1234567890abcd", createdAt))
	mock.ExpectExec(`(?s)UPDATE knowledge_qa_shares.*SET token_hash=.*WHERE id=.*source_workspace_id=.*owner_user_id=`).
		WithArgs(sqlmock.AnyArg(), "Owner", "Question?", "Answer [E1](agentteam-kqa-shared-evidence://E1).", now,
			"kqh_1234567890abcd", "wsp_1", "usr_owner").
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec(`DELETE FROM knowledge_qa_share_citations`).
		WithArgs("kqh_1234567890abcd").
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec(`(?s)INSERT INTO knowledge_qa_share_citations`).
		WithArgs(
			"kqh_1234567890abcd", "E1", "kb_1", "src_1", "primary", "primary",
			"sha-historical", "", "", "docs/design.md", int64(4), int64(5), now,
		).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectCommit()

	share, err := New(coredb.WrapSQLDB(sqlDB)).CreateShare(
		t.Context(), "wsp_1", "usr_owner", "kqs_1", "kqt_1", now,
	)
	require.NoError(t, err)
	require.Equal(t, "kqh_1234567890abcd", share.ID)
	require.Equal(t, createdAt, share.CreatedAt)
	require.Regexp(t, `^kqshare_[A-Za-z0-9_-]{43}$`, share.Token)
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestOpenShareRequiresValidTokenAndAttachesAuthenticatedRecipient(t *testing.T) {
	sqlDB, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer sqlDB.Close()
	now := time.Date(2026, 8, 14, 10, 0, 0, 0, time.UTC)
	token := knowledgeQAShareTokenPrefix + base64.RawURLEncoding.EncodeToString(make([]byte, knowledgeQAShareTokenBytes))
	tokenHash := sha256.Sum256([]byte(token))
	createdAt := now.Add(-time.Hour)

	mock.ExpectBegin()
	mock.ExpectQuery(`(?s)FROM knowledge_qa_shares AS share.*share.token_hash=`).
		WithArgs(tokenHash[:]).
		WillReturnRows(shareRows().AddRow(
			"kqh_1234567890abcd", "wsp_1", "org_1", "kqs_1", "kqt_1",
			"usr_owner", "Owner", "Question?", "Answer.", createdAt, createdAt, createdAt,
		))
	mock.ExpectExec(`(?s)INSERT INTO knowledge_qa_share_recipients.*ON DUPLICATE KEY UPDATE`).
		WithArgs("kqh_1234567890abcd", "usr_recipient", now, now).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectCommit()

	share, err := New(coredb.WrapSQLDB(sqlDB)).OpenShare(
		t.Context(), "usr_recipient", token, now,
	)
	require.NoError(t, err)
	require.Equal(t, "kqh_1234567890abcd", share.ID)
	require.Equal(t, now, share.LastOpenedAt)
	require.Empty(t, share.Token)
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestOpenShareRejectsMalformedTokenBeforeDatabaseAccess(t *testing.T) {
	sqlDB, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer sqlDB.Close()

	_, err = New(coredb.WrapSQLDB(sqlDB)).OpenShare(
		t.Context(), "usr_recipient", "kqshare_guessable", time.Now(),
	)
	require.ErrorIs(t, err, ErrNotFound)
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestLoadReceivedShareRequiresExactRecipientAndTouchesOnlyThatRow(t *testing.T) {
	sqlDB, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer sqlDB.Close()
	now := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC)
	createdAt := now.Add(-2 * time.Hour)

	mock.ExpectBegin()
	mock.ExpectQuery(`(?s)JOIN knowledge_qa_share_recipients AS recipient.*share.id=.*recipient.recipient_user_id=`).
		WithArgs("kqh_1234567890abcd", "usr_recipient").
		WillReturnRows(shareRows().AddRow(
			"kqh_1234567890abcd", "wsp_source", "org_source", "kqs_1", "kqt_1",
			"usr_owner", "Owner", "Question?", "Answer.", createdAt, createdAt, createdAt,
		))
	mock.ExpectExec(`(?s)UPDATE knowledge_qa_share_recipients.*last_opened_at=.*share_id=.*recipient_user_id=`).
		WithArgs(now, "kqh_1234567890abcd", "usr_recipient").
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectCommit()

	share, err := New(coredb.WrapSQLDB(sqlDB)).LoadReceivedShare(
		t.Context(), "usr_recipient", "kqh_1234567890abcd", now,
	)
	require.NoError(t, err)
	require.Equal(t, "kqh_1234567890abcd", share.ID)
	require.Equal(t, now, share.LastOpenedAt)
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestLoadReceivedShareCitationRequiresRecipientAndExactEvidenceMapping(t *testing.T) {
	sqlDB, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer sqlDB.Close()

	mock.ExpectQuery(`(?s)FROM knowledge_qa_shares AS share.*JOIN knowledge_qa_share_recipients.*JOIN knowledge_qa_share_citations.*citation.evidence_id=`).
		WithArgs("kqh_1234567890abcd", "usr_recipient", "E1").
		WillReturnRows(sqlmock.NewRows([]string{
			"source_workspace_id", "source_organization_id", "source_turn_id",
			"knowledge_base_id", "source_id", "root_kind", "repo_role",
			"revision", "resolved_ref", "resolved_ref_kind", "path", "line_start", "line_end",
		}).AddRow(
			"wsp_source", "org_source", "kqt_source", "kb_1", "src_1",
			"primary", "primary", "sha-historical", "main", "branch", "docs/design.md",
			4, 5,
		))

	shared, err := New(coredb.WrapSQLDB(sqlDB)).LoadReceivedShareCitation(
		t.Context(), "usr_recipient", "kqh_1234567890abcd", "E1",
	)
	require.NoError(t, err)
	require.Equal(t, "wsp_source", shared.SourceWorkspaceID)
	require.Equal(t, "org_source", shared.SourceOrganizationID)
	require.Equal(t, "kqt_source", shared.SourceTurnID)
	require.Equal(t, "sha-historical", shared.Citation.Revision)
	require.Equal(t, "docs/design.md", shared.Citation.Path)
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestListReceivedSharesUsesRecipientScopedBoundedQuery(t *testing.T) {
	sqlDB, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer sqlDB.Close()
	now := time.Date(2026, 8, 14, 10, 0, 0, 0, time.UTC)

	mock.ExpectQuery(`(?s)JOIN knowledge_qa_share_recipients AS recipient.*recipient.recipient_user_id=.*ORDER BY recipient.last_opened_at DESC.*LIMIT`).
		WithArgs("usr_recipient", 5).
		WillReturnRows(sqlmock.NewRows([]string{
			"id", "owner_display_name", "question", "created_at", "updated_at", "last_opened_at",
		}).AddRow("kqh_1234567890abcd", "Owner", "Question?", now, now, now))

	shares, err := New(coredb.WrapSQLDB(sqlDB)).ListReceivedShares(
		t.Context(), "usr_recipient", 5,
	)
	require.NoError(t, err)
	require.Len(t, shares, 1)
	require.Equal(t, "Question?", shares[0].Question)
	require.Empty(t, shares[0].Answer)
	require.Empty(t, shares[0].SourceWorkspaceID)
	require.NoError(t, mock.ExpectationsWereMet())
}

func shareRows() *sqlmock.Rows {
	return sqlmock.NewRows([]string{
		"id", "source_workspace_id", "source_organization_id", "source_session_id",
		"source_turn_id", "owner_user_id", "owner_display_name", "question", "answer",
		"created_at", "updated_at", "last_opened_at",
	})
}

func expectShareCitationLoad(mock sqlmock.Sqlmock, now time.Time) {
	mock.ExpectQuery(`(?s)FROM knowledge_qa_citations AS citation.*citation.id IN`).
		WithArgs("wsp_1", "kqt_1", "kqc_abcdefghijklmn").
		WillReturnRows(sqlmock.NewRows([]string{
			"id", "workspace_id", "turn_id", "attempt_id", "citation_kind",
			"knowledge_base_id", "source_id", "root_kind", "repo_role", "revision",
			"resolved_ref", "resolved_ref_kind", "path", "line_start", "line_end",
			"url", "title", "snippet", "fetched_at", "created_at",
		}).AddRow(
			"kqc_abcdefghijklmn", "wsp_1", "kqt_1", "kqa_attempt", "knowledge",
			"kb_1", "src_1", "primary", "primary", "sha-historical", "", "",
			"docs/design.md", 4, 5, "", "", "", nil, now,
		))
}
