// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

package db

import (
	"context"
	"errors"
	"testing"
	"time"

	sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc"
	mockdb "github.com/MeshCore-Beacon/beacon-server/db/sqlc/mock"
	"github.com/jackc/pgx/v5/pgtype"
	"go.uber.org/mock/gomock"
)

func TestListChannels_Empty(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	mock.EXPECT().
		ListChannels(gomock.Any(), sqlc.ListChannelsParams{
			ChannelHash: nil,
			Iatas:       nil,
			CursorTs:    pgtype.Timestamptz{},
			PageLimit:   11,
		}).
		Return([]sqlc.Channel{}, nil)

	store := &Store{q: mock}
	page, err := store.ListChannels(context.Background(), 10, nil, nil, 0)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(page.Items) != 0 {
		t.Errorf("expected 0 items, got %d", len(page.Items))
	}
	if page.HasMore {
		t.Error("expected HasMore false")
	}
}

func TestListChannels_Pagination(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	isHashtag := false
	keyKnown := true
	lastSeen := pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true}

	// return limit+1 rows to trigger HasMore
	rows := make([]sqlc.Channel, 3)
	for i := range rows {
		rows[i] = sqlc.Channel{
			ID:          int32(i + 1),
			ChannelHash: []byte{0xab, 0xcd},
			LastSeen:    lastSeen,
			IsHashtag:   &isHashtag,
			KeyKnown:    &keyKnown,
		}
	}

	mock.EXPECT().
		ListChannels(gomock.Any(), sqlc.ListChannelsParams{
			ChannelHash: nil,
			Iatas:       nil,
			CursorTs:    pgtype.Timestamptz{},
			PageLimit:   3, // limit+1
		}).
		Return(rows, nil)

	store := &Store{q: mock}
	page, err := store.ListChannels(context.Background(), 2, nil, nil, 0)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(page.Items) != 2 {
		t.Errorf("expected 2 items, got %d", len(page.Items))
	}
	if !page.HasMore {
		t.Error("expected HasMore true")
	}
	if page.NextCursor == nil {
		t.Error("expected NextCursor to be set")
	}
}

func TestListChannels_DBError(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	mock.EXPECT().
		ListChannels(gomock.Any(), gomock.Any()).
		Return(nil, errors.New("db error"))

	store := &Store{q: mock}
	_, err := store.ListChannels(context.Background(), 10, nil, nil, 0)
	if err == nil {
		t.Fatal("expected error, got nil")
	}
}

func TestListChannels_IATAFilter(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	mock.EXPECT().
		ListChannels(gomock.Any(), sqlc.ListChannelsParams{
			ChannelHash: nil,
			Iatas:       []string{"YOW", "YYZ"},
			CursorTs:    pgtype.Timestamptz{},
			PageLimit:   11,
		}).
		Return([]sqlc.Channel{}, nil)

	store := &Store{q: mock}
	_, err := store.ListChannels(context.Background(), 10, nil, []string{"YOW", "YYZ"}, 0)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestGetChannel_Basic(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	isHashtag := false
	keyKnown := true
	msgCount := int64(5)
	lastSeen := pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true}

	mock.EXPECT().
		GetChannelByID(gomock.Any(), int32(1)).
		Return(sqlc.Channel{
			ID:           1,
			ChannelHash:  []byte{0xab, 0xcd},
			LastSeen:     lastSeen,
			IsHashtag:    &isHashtag,
			KeyKnown:     &keyKnown,
			MessageCount: &msgCount,
		}, nil)

	store := &Store{q: mock}
	ch, err := store.GetChannel(context.Background(), 1)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if ch.ID != 1 {
		t.Errorf("expected ID 1, got %d", ch.ID)
	}
	if ch.ChannelHash != "abcd" {
		t.Errorf("expected ChannelHash abcd, got %s", ch.ChannelHash)
	}
	if ch.MessageCount != 5 {
		t.Errorf("expected MessageCount 5, got %d", ch.MessageCount)
	}
	if ch.KeyFingerprint != nil {
		t.Errorf("expected nil KeyFingerprint for non-hashtag channel")
	}
}

func TestGetChannel_HashtagWithFingerprint(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	isHashtag := true
	keyKnown := true
	fp := []byte{0xde, 0xad, 0xbe, 0xef}
	lastSeen := pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true}

	mock.EXPECT().
		GetChannelByID(gomock.Any(), int32(2)).
		Return(sqlc.Channel{
			ID:             2,
			ChannelHash:    []byte{0x01, 0x02},
			LastSeen:       lastSeen,
			IsHashtag:      &isHashtag,
			KeyKnown:       &keyKnown,
			KeyFingerprint: fp,
		}, nil)

	store := &Store{q: mock}
	ch, err := store.GetChannel(context.Background(), 2)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if ch.KeyFingerprint == nil {
		t.Fatal("expected KeyFingerprint to be set for hashtag channel")
	}
	if *ch.KeyFingerprint != "deadbeef" {
		t.Errorf("expected KeyFingerprint deadbeef, got %s", *ch.KeyFingerprint)
	}
}

func TestGetChannel_DBError(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	mock.EXPECT().
		GetChannelByID(gomock.Any(), int32(1)).
		Return(sqlc.Channel{}, errors.New("db error"))

	store := &Store{q: mock}
	_, err := store.GetChannel(context.Background(), 1)
	if err == nil {
		t.Fatal("expected error, got nil")
	}
}

func TestListChannelMessages_AllChannels(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	sentAt := pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true}
	senderName := "Alice"
	content := "hello"

	mock.EXPECT().
		ListAllChannelMessages(gomock.Any(), sqlc.ListAllChannelMessagesParams{
			Column1: pgtype.Timestamptz{},
			Column2: []string{"YVR"},
			Column3: "",
			Column4: int64(0),
			Limit:   3,
		}).
		Return([]sqlc.ListAllChannelMessagesRow{
			{
				ID:               1,
				PacketHashHex:    "deadbeef",
				ChannelHash:      []byte{0xab},
				SenderName:       &senderName,
				Content:          &content,
				SentAt:           sentAt,
				ObservationCount: 2,
			},
			{
				ID:               2,
				PacketHashHex:    "cafebabe",
				ChannelHash:      []byte{0xcd},
				SenderName:       &senderName,
				Content:          &content,
				SentAt:           sentAt,
				ObservationCount: 1,
			},
			{
				ID:               3,
				PacketHashHex:    "deadcafe",
				ChannelHash:      []byte{0xef},
				SenderName:       &senderName,
				Content:          &content,
				SentAt:           sentAt,
				ObservationCount: 1,
			},
		}, nil)

	store := &Store{q: mock}
	page, err := store.ListChannelMessages(context.Background(), nil, time.Time{}, 2, []string{"YVR"}, "", 0)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(page.Items) != 2 {
		t.Errorf("expected 2 items, got %d", len(page.Items))
	}
	if !page.HasMore {
		t.Error("expected HasMore true")
	}
	if page.NextCursor == nil {
		t.Error("expected NextCursor to be set")
	}
}

func TestListChannelMessages_ByChannelID(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	channelID := int32(1)
	sentAt := pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true}
	senderName := "Bob"
	content := "world"

	mock.EXPECT().
		ListChannelMessages(gomock.Any(), sqlc.ListChannelMessagesParams{
			ChannelID: channelID,
			Column2:   pgtype.Timestamptz{},
			Column3:   []string{"YVR"},
			Column4:   "",
			Column5:   int64(0),
			Limit:     3,
		}).
		Return([]sqlc.ListChannelMessagesRow{
			{
				ID:               1,
				PacketHashHex:    "deadbeef",
				ChannelHash:      []byte{0xab},
				SenderName:       &senderName,
				Content:          &content,
				SentAt:           sentAt,
				ObservationCount: 1,
			},
		}, nil)

	store := &Store{q: mock}
	page, err := store.ListChannelMessages(context.Background(), &channelID, time.Time{}, 2, []string{"YVR"}, "", 0)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(page.Items) != 1 {
		t.Errorf("expected 1 item, got %d", len(page.Items))
	}
	if page.HasMore {
		t.Error("expected HasMore false")
	}
}

func TestListChannelMessages_DBError(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	mock.EXPECT().
		ListAllChannelMessages(gomock.Any(), gomock.Any()).
		Return(nil, errors.New("db error"))

	store := &Store{q: mock}
	_, err := store.ListChannelMessages(context.Background(), nil, time.Time{}, 10, nil, "", 0)
	if err == nil {
		t.Fatal("expected error, got nil")
	}
}
