package store

import (
	"context"
	"errors"
	"path/filepath"
	"testing"
	"time"
)

// room — комната из трёх ников для тестов хранилища.
func newTestStore(t *testing.T, nicks ...string) (*Store, context.Context) {
	t.Helper()
	ctx := context.Background()
	s := open(t, filepath.Join(t.TempDir(), "bare.db"))
	for i, nick := range nicks {
		err := s.CreateUser(ctx, User{
			Nick:      nick,
			Cred:      Credential{Hash: []byte("hash"), Salt: []byte("salt"), Params: "argon2id,m=19456,t=2,p=1"},
			PublicKey: `{"kty":"EC"}`,
			KeyBlob:   `{"v":1}`,
			CreatedAt: int64(i + 1),
		})
		if err != nil {
			t.Fatalf("CreateUser %s: %v", nick, err)
		}
	}
	return s, ctx
}

// wrap — завёрнутый ключ участнику: содержимое хранилищу безразлично.
func wrap(nicks ...string) []WrappedKey {
	out := make([]WrappedKey, 0, len(nicks))
	for _, nick := range nicks {
		out = append(out, WrappedKey{To: nick, IV: "iv-" + nick, CT: "ct-" + nick})
	}
	return out
}

// rekey — смена состава и раздача нового ключа.
func rekey(t *testing.T, s *Store, ctx context.Context, room, owner, keyID string, add, remove, to []string, now int64) RoomChange {
	t.Helper()
	change, err := s.UpdateMembers(ctx, MembersChange{
		RoomID: room,
		Owner:  owner,
		Add:    add,
		Remove: remove,
		KeyID:  keyID,
		Keys:   wrap(to...),
		Now:    now,
	})
	if err != nil {
		t.Fatalf("UpdateMembers %s: %v", keyID, err)
	}
	return change
}

// makeRoom — комната с одним владельцем и его ключом.
func makeRoom(t *testing.T, s *Store, ctx context.Context, owner string, now int64) RoomChange {
	t.Helper()
	change, err := s.CreateRoom(ctx, NewRoom{
		ID:    "room-1",
		Name:  "общая",
		Owner: owner,
		KeyID: "k1",
		Key:   wrap(owner)[0],
		Now:   now,
	})
	if err != nil {
		t.Fatalf("CreateRoom: %v", err)
	}
	return change
}

// Занятый идентификатор комнаты — ErrRoomExists, и ни одной строки
// существующая комната при этом не теряет (ADR-037).
func TestCreateRoomTakenID(t *testing.T) {
	s, ctx := newTestStore(t, "marta", "petya")
	makeRoom(t, s, ctx, "marta", 1000)

	_, err := s.CreateRoom(ctx, NewRoom{ID: "room-1", Name: "чужая", Owner: "petya",
		KeyID: "k2", Key: wrap("petya")[0], Now: 2000})
	if !errors.Is(err, ErrRoomExists) {
		t.Fatalf("CreateRoom с занятым id: получено %v, ожидалось ErrRoomExists", err)
	}
	rooms, err := s.Rooms(ctx, "marta")
	if err != nil {
		t.Fatalf("Rooms: %v", err)
	}
	if len(rooms) != 1 || rooms[0].Name != "общая" || rooms[0].Owner != "marta" ||
		current(rooms[0]) == nil || current(rooms[0]).KeyID != "k1" {
		t.Errorf("комната после отказа: %+v", rooms)
	}
	if got, err := s.Rooms(ctx, "petya"); err != nil || len(got) != 0 {
		t.Errorf("занятый id присоединил чужого: %+v, %v", got, err)
	}
}

// current — текущий ключ участника: последний в Keys, они идут от старого
// к новому (ADR-059).
func current(r Room) *RoomKey {
	if len(r.Keys) == 0 {
		return nil
	}
	return &r.Keys[len(r.Keys)-1]
}

// У комнаты живут два последних keyId; обрезка при rekey и фоновая чистка
// держат одни и те же ключи и не трогают текущий ключ участника (ADR-018).
func TestRoomKeysTrimmedToTwo(t *testing.T) {
	s, ctx := newTestStore(t, "marta", "petya", "kolya")
	makeRoom(t, s, ctx, "marta", 1000)
	rekey(t, s, ctx, "room-1", "marta", "k2", []string{"petya"}, nil, []string{"marta", "petya"}, 2000)
	rekey(t, s, ctx, "room-1", "marta", "k3", []string{"kolya"}, nil, []string{"marta", "petya", "kolya"}, 3000)

	if got := ids(t, s, `SELECT DISTINCT key_id FROM room_keys ORDER BY key_id`); !equal(got, []string{"k2", "k3"}) {
		t.Errorf("ключи после rekey: получено %v, ожидалось [k2 k3]", got)
	}
	// Фоновая чистка ничего не добавляет к обрезке: запрос у них один.
	if err := s.Cleanup(ctx, time.Now()); err != nil {
		t.Fatalf("Cleanup: %v", err)
	}
	if got := ids(t, s, `SELECT DISTINCT key_id FROM room_keys ORDER BY key_id`); !equal(got, []string{"k2", "k3"}) {
		t.Errorf("ключи после чистки: получено %v, ожидалось [k2 k3]", got)
	}
	// Текущий ключ есть у каждого участника, и он последний.
	for _, nick := range []string{"marta", "petya", "kolya"} {
		rooms, err := s.Rooms(ctx, nick)
		if err != nil {
			t.Fatalf("Rooms %s: %v", nick, err)
		}
		if len(rooms) != 1 || current(rooms[0]) == nil {
			t.Fatalf("комнаты %s: %+v", nick, rooms)
		}
		if current(rooms[0]).KeyID != "k3" || current(rooms[0]).CT != "ct-"+nick {
			t.Errorf("ключ %s: %+v", nick, current(rooms[0]))
		}
	}
	// Участнику отдаются оба удерживаемых ключа, от старого к новому:
	// без прежнего он не прочитает сообщение, отправленное до последнего
	// rekey, пока его не было (ADR-059).
	rooms, err := s.Rooms(ctx, "petya")
	if err != nil {
		t.Fatalf("Rooms petya: %v", err)
	}
	if len(rooms) != 1 || len(rooms[0].Keys) != 2 {
		t.Fatalf("ключи petya: %+v", rooms)
	}
	if rooms[0].Keys[0].KeyID != "k2" || rooms[0].Keys[1].KeyID != "k3" {
		t.Errorf("порядок ключей: получено %v, ожидалось [k2 k3]",
			[]string{rooms[0].Keys[0].KeyID, rooms[0].Keys[1].KeyID})
	}
	// Ключей чужой комнаты в ответе нет.
	if got, err := s.Rooms(ctx, "marta"); err != nil || len(got) != 1 || len(got[0].Keys) != 2 {
		t.Errorf("ключи marta: %+v, %v", got, err)
	}
}

// Два rekey в одну миллисекунду: текущим остаётся последний розданный ключ,
// и обрезка его не выбрасывает (docs/storage.md, «Текущий ключ комнаты»).
// Идентификаторы ключей случайны, поэтому свежий вполне может оказаться
// меньше прежнего по порядку сортировки — на этом и построен случай.
func TestRoomKeysWithinOneMillisecond(t *testing.T) {
	s, ctx := newTestStore(t, "marta", "petya")
	if _, err := s.CreateRoom(ctx, NewRoom{ID: "room-1", Name: "общая", Owner: "marta",
		KeyID: "ccc", Key: wrap("marta")[0], Now: 1000}); err != nil {
		t.Fatalf("CreateRoom: %v", err)
	}
	rekey(t, s, ctx, "room-1", "marta", "bbb", []string{"petya"}, nil, []string{"marta", "petya"}, 1000)
	rekey(t, s, ctx, "room-1", "marta", "aaa", nil, nil, []string{"marta", "petya"}, 1000)

	if got := ids(t, s, `SELECT DISTINCT key_id FROM room_keys ORDER BY key_id`); !equal(got, []string{"aaa", "bbb"}) {
		t.Errorf("ключи: получено %v, ожидалось [aaa bbb]", got)
	}
	for _, nick := range []string{"marta", "petya"} {
		rooms, err := s.Rooms(ctx, nick)
		if err != nil || len(rooms) != 1 || current(rooms[0]) == nil {
			t.Fatalf("комнаты %s: %+v, %v", nick, rooms, err)
		}
		if current(rooms[0]).KeyID != "aaa" {
			t.Errorf("текущий ключ %s: получено %q, ожидалось \"aaa\"", nick, current(rooms[0]).KeyID)
		}
	}
	// Свежим ключом можно писать: он остался ключом комнаты.
	access, err := s.RoomAccess(ctx, "room-1", "marta", "aaa")
	if err != nil || !access.Member || !access.KnownKey {
		t.Errorf("доступ по свежему ключу: %+v, %v", access, err)
	}
	if access.Name != "общая" {
		t.Errorf("имя комнаты: получено %q, ожидалось \"общая\"", access.Name)
	}
}

// Убранный участник теряет и членство, и все свои ключи.
func TestRoomRemoveDropsKeys(t *testing.T) {
	s, ctx := newTestStore(t, "marta", "petya")
	makeRoom(t, s, ctx, "marta", 1000)
	rekey(t, s, ctx, "room-1", "marta", "k2", []string{"petya"}, nil, []string{"marta", "petya"}, 2000)

	change := rekey(t, s, ctx, "room-1", "marta", "k3", nil, []string{"petya"}, []string{"marta"}, 3000)
	if len(change.Left) != 1 || change.Left[0].Nick != "petya" {
		t.Errorf("выбывшие: %+v", change.Left)
	}
	if got := ids(t, s, `SELECT nick FROM room_keys ORDER BY nick, key_id`); !equal(got, []string{"marta", "marta"}) {
		t.Errorf("ключи после удаления участника: %v", got)
	}
	rooms, err := s.Rooms(ctx, "petya")
	if err != nil || len(rooms) != 0 {
		t.Errorf("комнаты убранного: %+v, %v", rooms, err)
	}
}

// Отказ в середине не оставляет следов: состав и ключи те же (docs/protocol.md).
func TestRoomChangeIsAtomic(t *testing.T) {
	s, ctx := newTestStore(t, "marta", "petya", "kolya")
	makeRoom(t, s, ctx, "marta", 1000)
	rekey(t, s, ctx, "room-1", "marta", "k2", []string{"petya"}, nil, []string{"marta", "petya"}, 2000)

	cases := []struct {
		name string
		c    MembersChange
		want error
	}{
		{"не владелец", MembersChange{RoomID: "room-1", Owner: "petya", KeyID: "k3",
			Keys: wrap("marta", "petya"), Now: 3000}, ErrNotOwner},
		{"нет комнаты", MembersChange{RoomID: "нет", Owner: "marta", KeyID: "k3",
			Keys: wrap("marta", "petya"), Now: 3000}, ErrNotOwner},
		{"нет ника", MembersChange{RoomID: "room-1", Owner: "marta", Add: []string{"никого"}, KeyID: "k3",
			Keys: wrap("marta", "petya", "никого"), Now: 3000}, ErrUnknownUser},
		{"не участник", MembersChange{RoomID: "room-1", Owner: "marta", Remove: []string{"kolya"}, KeyID: "k3",
			Keys: wrap("marta", "petya"), Now: 3000}, ErrNotMember},
		{"владелец", MembersChange{RoomID: "room-1", Owner: "marta", Remove: []string{"marta"}, KeyID: "k3",
			Keys: wrap("petya"), Now: 3000}, ErrOwnerRemoval},
		{"ключ уже был", MembersChange{RoomID: "room-1", Owner: "marta", Add: []string{"kolya"}, KeyID: "k2",
			Keys: wrap("marta", "petya", "kolya"), Now: 3000}, ErrKeyExists},
		{"ключи не по составу", MembersChange{RoomID: "room-1", Owner: "marta", Add: []string{"kolya"}, KeyID: "k3",
			Keys: wrap("marta", "petya"), Now: 3000}, ErrKeysMismatch},
		// Два ключа одному вместо ключа другому: длина сходится, состав — нет.
		{"два ключа одному", MembersChange{RoomID: "room-1", Owner: "marta", KeyID: "k3",
			Keys: wrap("marta", "marta"), Now: 3000}, ErrKeysMismatch},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			if _, err := s.UpdateMembers(ctx, c.c); !errors.Is(err, c.want) {
				t.Fatalf("получено %v, ожидалось %v", err, c.want)
			}
			if got := ids(t, s, `SELECT nick FROM room_members WHERE room_id = 'room-1' ORDER BY nick`); !equal(got, []string{"marta", "petya"}) {
				t.Errorf("состав: получено %v, ожидалось [marta petya]", got)
			}
			if got := ids(t, s, `SELECT DISTINCT key_id FROM room_keys ORDER BY key_id`); !equal(got, []string{"k1", "k2"}) {
				t.Errorf("ключи: получено %v, ожидалось [k1 k2]", got)
			}
		})
	}
}

// Удаление аккаунта передаёт владение участнику с наименьшим joined_at,
// а комнату без участников удаляет (ADR-018).
func TestDeleteUserRooms(t *testing.T) {
	s, ctx := newTestStore(t, "marta", "petya", "kolya")
	makeRoom(t, s, ctx, "marta", 1000)
	rekey(t, s, ctx, "room-1", "marta", "k2", []string{"kolya"}, nil, []string{"marta", "kolya"}, 2000)
	rekey(t, s, ctx, "room-1", "marta", "k3", []string{"petya"}, nil, []string{"marta", "kolya", "petya"}, 3000)

	// Вторая комната — только владелец.
	if _, err := s.CreateRoom(ctx, NewRoom{ID: "room-2", Name: "своя", Owner: "marta",
		KeyID: "k1", Key: wrap("marta")[0], Now: 4000}); err != nil {
		t.Fatalf("CreateRoom: %v", err)
	}

	if _, err := s.DeleteUser(ctx, "marta"); err != nil {
		t.Fatalf("DeleteUser: %v", err)
	}
	if got := ids(t, s, `SELECT id FROM rooms ORDER BY id`); !equal(got, []string{"room-1"}) {
		t.Errorf("комнаты: получено %v, ожидалось [room-1]", got)
	}
	if got := ids(t, s, `SELECT owner FROM rooms`); !equal(got, []string{"kolya"}) {
		t.Errorf("владелец: получено %v, ожидалось [kolya]", got)
	}
	if got := ids(t, s, `SELECT nick FROM room_members ORDER BY nick`); !equal(got, []string{"kolya", "petya"}) {
		t.Errorf("состав: получено %v, ожидалось [kolya petya]", got)
	}
	if got := ids(t, s, `SELECT DISTINCT nick FROM room_keys ORDER BY nick`); !equal(got, []string{"kolya", "petya"}) {
		t.Errorf("ключи: получено %v, ожидалось [kolya petya]", got)
	}
}

// Удаление аккаунта простого участника чужую комнату не трогает: членство
// и ключи уносит каскад, владелец и остальные на месте.
func TestDeleteUserMember(t *testing.T) {
	s, ctx := newTestStore(t, "marta", "petya")
	if _, err := s.CreateRoom(ctx, NewRoom{ID: "room-1", Name: "общая", Owner: "petya",
		KeyID: "k1", Key: wrap("petya")[0], Now: 1000}); err != nil {
		t.Fatalf("CreateRoom: %v", err)
	}
	rekey(t, s, ctx, "room-1", "petya", "k2", []string{"marta"}, nil, []string{"petya", "marta"}, 2000)

	if _, err := s.DeleteUser(ctx, "marta"); err != nil {
		t.Fatalf("DeleteUser: %v", err)
	}
	if got := ids(t, s, `SELECT owner FROM rooms`); !equal(got, []string{"petya"}) {
		t.Errorf("комнаты: получено %v, ожидалось [petya]", got)
	}
	if got := ids(t, s, `SELECT nick FROM room_members`); !equal(got, []string{"petya"}) {
		t.Errorf("состав: получено %v, ожидалось [petya]", got)
	}
	if got := ids(t, s, `SELECT DISTINCT nick FROM room_keys`); !equal(got, []string{"petya"}) {
		t.Errorf("ключи: получено %v, ожидалось [petya]", got)
	}
}

// Выход владельца передаёт владение; выход последнего удаляет комнату.
func TestLeaveRoom(t *testing.T) {
	s, ctx := newTestStore(t, "marta", "petya")
	makeRoom(t, s, ctx, "marta", 1000)
	rekey(t, s, ctx, "room-1", "marta", "k2", []string{"petya"}, nil, []string{"marta", "petya"}, 2000)

	// Не участник — ErrNotFound, комната не тронута.
	if _, err := s.LeaveRoom(ctx, "нет", "marta"); !errors.Is(err, ErrNotFound) {
		t.Errorf("выход из несуществующей комнаты: %v", err)
	}

	change, err := s.LeaveRoom(ctx, "room-1", "marta")
	if err != nil {
		t.Fatalf("LeaveRoom: %v", err)
	}
	if change.Room.Owner != "petya" {
		t.Errorf("владелец: получено %q, ожидалось \"petya\"", change.Room.Owner)
	}
	if len(change.Members) != 1 || change.Members[0].Nick != "petya" {
		t.Fatalf("оставшиеся: %+v", change.Members)
	}
	if change.Members[0].Key == nil || change.Members[0].Key.KeyID != "k2" {
		t.Errorf("ключ оставшегося: %+v", change.Members[0].Key)
	}
	if !change.Room.NeedsRekey {
		t.Error("needsRekey после выхода участника")
	}
	if len(change.Left) != 1 || change.Left[0].Nick != "marta" {
		t.Errorf("room_left вышедшему: %+v", change.Left)
	}
	if got := ids(t, s, `SELECT DISTINCT nick FROM room_keys`); !equal(got, []string{"petya"}) {
		t.Errorf("ключи после выхода: %v", got)
	}

	last, err := s.LeaveRoom(ctx, "room-1", "petya")
	if err != nil {
		t.Fatalf("LeaveRoom: %v", err)
	}
	// Комнаты больше нет: room слать некому, room_left уходит другим
	// устройствам вышедшего (ADR-041).
	if len(last.Members) != 0 {
		t.Errorf("получатели после выхода последнего: %+v", last.Members)
	}
	if len(last.Left) != 1 || last.Left[0].Nick != "petya" {
		t.Errorf("room_left после выхода последнего: %+v", last.Left)
	}
	if got := ids(t, s, `SELECT id FROM rooms`); len(got) != 0 {
		t.Errorf("пустая комната осталась: %v", got)
	}
	if got := ids(t, s, `SELECT DISTINCT nick FROM room_keys`); len(got) != 0 {
		t.Errorf("ключи удалённой комнаты остались: %v", got)
	}
}
