package coplay

import (
	"context"
	"io"
	"log/slog"
	"os"
	"testing"
	"time"

	"github.com/jackc/pgx/v5/pgtype"
	"github.com/jackc/pgx/v5/pgxpool"

	"git.fabledsword.com/bvandeusen/minstrel/internal/db"
	"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
	"git.fabledsword.com/bvandeusen/minstrel/internal/dbtest"
)

func testPool(t *testing.T) *pgxpool.Pool {
	t.Helper()
	if testing.Short() {
		t.Skip("skipping in -short mode")
	}
	dsn := os.Getenv("MINSTREL_TEST_DATABASE_URL")
	if dsn == "" {
		t.Skip("MINSTREL_TEST_DATABASE_URL not set")
	}
	if err := db.Migrate(dsn, slog.New(slog.NewTextHandler(io.Discard, nil))); err != nil {
		t.Fatalf("migrate: %v", err)
	}
	pool, err := pgxpool.New(context.Background(), dsn)
	if err != nil {
		t.Fatalf("pool: %v", err)
	}
	t.Cleanup(pool.Close)
	dbtest.ResetDB(t, pool)
	return pool
}

func discardLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) }

func seedUser(t *testing.T, q *dbq.Queries, name string) pgtype.UUID {
	t.Helper()
	u, err := q.CreateUser(context.Background(), dbq.CreateUserParams{
		Username: dbtest.TestUserPrefix + name, PasswordHash: "x", ApiToken: name + "-tok", IsAdmin: false,
	})
	if err != nil {
		t.Fatalf("seed user %s: %v", name, err)
	}
	return u.ID
}

// seedArtistTrack creates an artist + album + one track, returning both ids.
func seedArtistTrack(t *testing.T, q *dbq.Queries, name string) (pgtype.UUID, pgtype.UUID) {
	t.Helper()
	ctx := context.Background()
	a, err := q.UpsertArtist(ctx, dbq.UpsertArtistParams{Name: name, SortName: name})
	if err != nil {
		t.Fatalf("seed artist: %v", err)
	}
	al, err := q.UpsertAlbum(ctx, dbq.UpsertAlbumParams{Title: name, SortTitle: name, ArtistID: a.ID})
	if err != nil {
		t.Fatalf("seed album: %v", err)
	}
	tr, err := q.UpsertTrack(ctx, dbq.UpsertTrackParams{
		Title: name, AlbumID: al.ID, ArtistID: a.ID,
		DurationMs: 1000, FilePath: "/tmp/coplay-" + name + ".mp3",
		FileSize: 1, FileFormat: "mp3",
	})
	if err != nil {
		t.Fatalf("seed track: %v", err)
	}
	return a.ID, tr.ID
}

func play(t *testing.T, pool *pgxpool.Pool, userID, trackID pgtype.UUID) {
	t.Helper()
	ctx := context.Background()
	at := time.Now()
	var sessionID pgtype.UUID
	if err := pool.QueryRow(ctx,
		`INSERT INTO play_sessions (user_id, started_at, last_event_at, client_id)
		 VALUES ($1, $2, $2, 'coplay-test') RETURNING id`,
		userID, at).Scan(&sessionID); err != nil {
		t.Fatalf("insert play_session: %v", err)
	}
	if _, err := pool.Exec(ctx,
		`INSERT INTO play_events (user_id, track_id, session_id, started_at, was_skipped)
		 VALUES ($1, $2, $3, $4, false)`,
		userID, trackID, sessionID, at); err != nil {
		t.Fatalf("insert play_event: %v", err)
	}
}

func coplayEdgeCount(t *testing.T, pool *pgxpool.Pool) int {
	t.Helper()
	var n int
	if err := pool.QueryRow(context.Background(),
		`SELECT count(*) FROM artist_similarity WHERE source='user_cooccurrence'`).Scan(&n); err != nil {
		t.Fatalf("count edges: %v", err)
	}
	return n
}

// TestCoplayWorker_BuildsEdges: two users who both play two artists produce a
// perfect-overlap co-play edge (Jaccard 1.0) in each direction.
func TestCoplayWorker_BuildsEdges(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	q := dbq.New(pool)

	u1 := seedUser(t, q, "cop1")
	u2 := seedUser(t, q, "cop2")
	aA, tA := seedArtistTrack(t, q, "ArtA")
	aB, tB := seedArtistTrack(t, q, "ArtB")

	for _, u := range []pgtype.UUID{u1, u2} {
		play(t, pool, u, tA)
		play(t, pool, u, tB)
	}

	if err := NewWorker(pool, discardLogger()).rebuild(ctx); err != nil {
		t.Fatalf("rebuild: %v", err)
	}

	var score float64
	if err := pool.QueryRow(ctx,
		`SELECT score FROM artist_similarity
		 WHERE artist_a_id=$1 AND artist_b_id=$2 AND source='user_cooccurrence'`,
		aA, aB).Scan(&score); err != nil {
		t.Fatalf("expected co-play edge ArtA→ArtB: %v", err)
	}
	// Jaccard of perfectly-overlapping player sets (both users play both):
	// coplayers=2, players each=2 → 2/(2+2-2) = 1.0.
	if score != 1.0 {
		t.Errorf("edge score = %v, want 1.0 (perfect overlap)", score)
	}
	if got := coplayEdgeCount(t, pool); got != 2 {
		t.Errorf("edge count = %d, want 2 (A→B and B→A)", got)
	}
}

// TestCoplayWorker_SingleUserNoEdges: one user's own co-listening yields no
// edges — the >= 2 distinct-co-player gate keeps single-user servers empty.
func TestCoplayWorker_SingleUserNoEdges(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	q := dbq.New(pool)

	u := seedUser(t, q, "solo")
	_, tA := seedArtistTrack(t, q, "SoloA")
	_, tB := seedArtistTrack(t, q, "SoloB")
	play(t, pool, u, tA)
	play(t, pool, u, tB)

	if err := NewWorker(pool, discardLogger()).rebuild(ctx); err != nil {
		t.Fatalf("rebuild: %v", err)
	}
	if got := coplayEdgeCount(t, pool); got != 0 {
		t.Errorf("single-user edge count = %d, want 0 (>= 2 co-player gate)", got)
	}
}
