package store

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

func open(t *testing.T, path string) *Store {
	t.Helper()
	s, err := Open(path)
	if err != nil {
		t.Fatalf("Open: %v", err)
	}
	t.Cleanup(func() { s.Close() })
	return s
}

func TestMigrateAndRestart(t *testing.T) {
	path := filepath.Join(t.TempDir(), "bare.db")

	first := open(t, path)
	want := []string{"001_init.sql", "002_room_needs_rekey.sql"}
	if got := first.Applied(); !equal(got, want) {
		t.Fatalf("применённые миграции: получено %v, ожидалось %v", got, want)
	}
	if got := version(t, first); got != len(want) {
		t.Errorf("user_version: получено %d, ожидалась %d", got, len(want))
	}
	// Все восемь таблиц из docs/storage.md на месте.
	for _, table := range []string{"users", "devices", "sessions", "contacts", "rooms", "room_members", "room_keys", "queue"} {
		var name string
		err := first.db.QueryRow(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, table).Scan(&name)
		if err != nil {
			t.Errorf("таблица %s: %v", table, err)
		}
	}
	// Внешние ключи включены — иначе каскадные удаления молча не работают.
	var fk int
	if err := first.db.QueryRow("PRAGMA foreign_keys").Scan(&fk); err != nil || fk != 1 {
		t.Errorf("foreign_keys: получено %d (%v), ожидалась 1", fk, err)
	}
	var mode string
	if err := first.db.QueryRow("PRAGMA journal_mode").Scan(&mode); err != nil || mode != "wal" {
		t.Errorf("journal_mode: получено %q (%v), ожидался wal", mode, err)
	}
	first.Close()

	// Повторный старт на той же базе ничего не применяет.
	second := open(t, path)
	if got := second.Applied(); len(got) != 0 {
		t.Errorf("повторный старт применил %v, ожидалось ничего", got)
	}
	if got := version(t, second); got != len(want) {
		t.Errorf("user_version после перезапуска: получено %d, ожидалась %d", got, len(want))
	}
}

func version(t *testing.T, s *Store) int {
	t.Helper()
	var v int
	if err := s.db.QueryRow("PRAGMA user_version").Scan(&v); err != nil {
		t.Fatalf("user_version: %v", err)
	}
	return v
}

func TestUsersAndSessions(t *testing.T) {
	ctx := context.Background()
	s := open(t, filepath.Join(t.TempDir(), "bare.db"))

	u := User{
		Nick:      "marta",
		Cred:      Credential{Hash: []byte("hash"), Salt: []byte("salt"), Params: "argon2id,m=19456,t=2,p=1"},
		PublicKey: `{"kty":"EC"}`,
		KeyBlob:   `{"v":1}`,
		CreatedAt: 1,
	}
	if err := s.CreateUser(ctx, u); err != nil {
		t.Fatalf("CreateUser: %v", err)
	}
	if err := s.CreateUser(ctx, u); !errors.Is(err, ErrNickTaken) {
		t.Errorf("повторный ник: получено %v, ожидалось ErrNickTaken", err)
	}
	if _, err := s.User(ctx, "нет-такого"); !errors.Is(err, ErrNotFound) {
		t.Errorf("чужой ник: получено %v, ожидалось ErrNotFound", err)
	}

	now := time.Now().UnixMilli()
	live := []byte("token-hash-1")
	other := []byte("token-hash-2")
	if err := s.CreateSession(ctx, live, "marta", now, now+1000); err != nil {
		t.Fatalf("CreateSession: %v", err)
	}
	if err := s.CreateSession(ctx, other, "marta", now, now+1000); err != nil {
		t.Fatalf("CreateSession: %v", err)
	}
	sess, err := s.Session(ctx, live, now)
	if err != nil || sess.Nick != "marta" || sess.DeviceID != "" {
		t.Fatalf("Session: %+v, %v", sess, err)
	}
	if _, err := s.Session(ctx, live, now+2000); !errors.Is(err, ErrNotFound) {
		t.Errorf("истёкшая сессия: получено %v, ожидалось ErrNotFound", err)
	}

	// Смена пароля с logoutOthers: остаётся только текущая сессия, а
	// устройства завершённых сессий отдаются обработчику — он закроет
	// их потоки событий (ADR-058).
	if _, err := s.RegisterDevice(ctx, "device-live", "marta", live, now); err != nil {
		t.Fatalf("RegisterDevice: %v", err)
	}
	if _, err := s.RegisterDevice(ctx, "device-other", "marta", other, now); err != nil {
		t.Fatalf("RegisterDevice: %v", err)
	}
	cred := Credential{Hash: []byte("new"), Salt: []byte("salt2"), Params: "argon2id,m=19456,t=2,p=1"}
	revoked, err := s.SetPassword(ctx, "marta", cred, `{"v":1,"new":true}`, true, live)
	if err != nil {
		t.Fatalf("SetPassword: %v", err)
	}
	if len(revoked) != 1 || revoked[0] != "device-other" {
		t.Errorf("устройства завершённых сессий: получено %v, ожидалось [device-other]", revoked)
	}
	if _, err := s.Session(ctx, other, now); !errors.Is(err, ErrNotFound) {
		t.Errorf("чужая сессия после logoutOthers: получено %v, ожидалось ErrNotFound", err)
	}
	if _, err := s.Session(ctx, live, now); err != nil {
		t.Errorf("текущая сессия после logoutOthers: %v", err)
	}
	got, err := s.User(ctx, "marta")
	if err != nil {
		t.Fatalf("User: %v", err)
	}
	if string(got.Cred.Hash) != "new" || got.KeyBlob != `{"v":1,"new":true}` {
		t.Errorf("хеш и блоб: получено %q / %q", got.Cred.Hash, got.KeyBlob)
	}

	// Удаление пользователя уносит сессии каскадом.
	if _, err := s.DeleteUser(ctx, "marta"); err != nil {
		t.Fatalf("DeleteUser: %v", err)
	}
	if _, err := s.Session(ctx, live, now); !errors.Is(err, ErrNotFound) {
		t.Errorf("сессия после удаления аккаунта: получено %v, ожидалось ErrNotFound", err)
	}
}

func TestCleanup(t *testing.T) {
	ctx := context.Background()
	s := open(t, filepath.Join(t.TempDir(), "bare.db"))
	now := time.Now()
	ms := now.UnixMilli()
	day := int64(24 * 60 * 60 * 1000)

	exec := func(query string, args ...any) {
		t.Helper()
		if _, err := s.db.ExecContext(ctx, query, args...); err != nil {
			t.Fatalf("%s: %v", query, err)
		}
	}
	exec(`INSERT INTO users (nick, auth_hash, auth_salt, auth_params, public_key, key_blob, created_at)
	      VALUES ('marta', x'00', x'00', 'argon2id,m=19456,t=2,p=1', '{}', '{}', ?)`, ms)
	exec(`INSERT INTO devices (id, nick, created_at, last_seen) VALUES ('old', 'marta', ?, ?)`, ms, ms-100*day)
	exec(`INSERT INTO devices (id, nick, created_at, last_seen) VALUES ('new', 'marta', ?, ?)`, ms, ms)
	exec(`INSERT INTO queue (device_id, msg_id, envelope, created_at) VALUES ('old', 'cascade', '{}', ?)`, ms)
	exec(`INSERT INTO queue (device_id, msg_id, envelope, created_at) VALUES ('new', 'stale', '{}', ?)`, ms-40*day)
	exec(`INSERT INTO queue (device_id, msg_id, envelope, created_at) VALUES ('new', 'fresh', '{}', ?)`, ms)
	exec(`INSERT INTO sessions (token_hash, nick, created_at, expires_at) VALUES (x'01', 'marta', ?, ?)`, ms, ms-day)
	exec(`INSERT INTO sessions (token_hash, nick, created_at, expires_at) VALUES (x'02', 'marta', ?, ?)`, ms, ms+day)
	exec(`INSERT INTO rooms (id, name, owner, created_at) VALUES ('r', 'общая', 'marta', ?)`, ms)
	for i, key := range []string{"k1", "k2", "k3"} {
		exec(`INSERT INTO room_keys (room_id, nick, key_id, sender, iv, ct, created_at)
		      VALUES ('r', 'marta', ?, 'marta', 'iv', 'ct', ?)`, key, ms+int64(i))
	}

	if err := s.Cleanup(ctx, now); err != nil {
		t.Fatalf("Cleanup: %v", err)
	}

	if got := ids(t, s, `SELECT id FROM devices ORDER BY id`); !equal(got, []string{"new"}) {
		t.Errorf("устройства: получено %v, ожидалось [new]", got)
	}
	// Очередь устройства 'old' ушла каскадом вместе с ним, 'stale' — по сроку.
	if got := ids(t, s, `SELECT msg_id FROM queue ORDER BY msg_id`); !equal(got, []string{"fresh"}) {
		t.Errorf("очередь: получено %v, ожидалось [fresh]", got)
	}
	if got := ids(t, s, `SELECT hex(token_hash) FROM sessions ORDER BY token_hash`); !equal(got, []string{"02"}) {
		t.Errorf("сессии: получено %v, ожидалось [02]", got)
	}
	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)
	}
}

func ids(t *testing.T, s *Store, query string) []string {
	t.Helper()
	rows, err := s.db.Query(query)
	if err != nil {
		t.Fatalf("%s: %v", query, err)
	}
	defer rows.Close()
	var out []string
	for rows.Next() {
		var v string
		if err := rows.Scan(&v); err != nil {
			t.Fatalf("scan: %v", err)
		}
		out = append(out, v)
	}
	if err := rows.Err(); err != nil {
		t.Fatalf("rows: %v", err)
	}
	return out
}

func equal(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}
