package db

import (
	"errors"
	"testing"
)

// seedTestRepo inserts a repository, manifest, tag, blob, and manifest_blob
// link for use in repository query tests. It returns the repository ID.
func seedTestRepo(t *testing.T, d *DB) int64 {
	t.Helper()

	_, err := d.Exec(`INSERT INTO repositories (name) VALUES ('myorg/myapp')`)
	if err != nil {
		t.Fatalf("insert repo: %v", err)
	}

	var repoID int64
	if err := d.QueryRow(`SELECT id FROM repositories WHERE name = 'myorg/myapp'`).Scan(&repoID); err != nil {
		t.Fatalf("select repo id: %v", err)
	}

	_, err = d.Exec(
		`INSERT INTO manifests (repository_id, digest, media_type, content, size)
		 VALUES (?, 'sha256:aaaa', 'application/vnd.oci.image.manifest.v1+json', '{"layers":[]}', 15)`,
		repoID,
	)
	if err != nil {
		t.Fatalf("insert manifest: %v", err)
	}

	var manifestID int64
	if err := d.QueryRow(`SELECT id FROM manifests WHERE digest = 'sha256:aaaa'`).Scan(&manifestID); err != nil {
		t.Fatalf("select manifest id: %v", err)
	}

	_, err = d.Exec(
		`INSERT INTO tags (repository_id, name, manifest_id) VALUES (?, 'latest', ?)`,
		repoID, manifestID,
	)
	if err != nil {
		t.Fatalf("insert tag: %v", err)
	}

	_, err = d.Exec(`INSERT INTO blobs (digest, size) VALUES ('sha256:bbbb', 2048)`)
	if err != nil {
		t.Fatalf("insert blob: %v", err)
	}

	var blobID int64
	if err := d.QueryRow(`SELECT id FROM blobs WHERE digest = 'sha256:bbbb'`).Scan(&blobID); err != nil {
		t.Fatalf("select blob id: %v", err)
	}

	_, err = d.Exec(
		`INSERT INTO manifest_blobs (manifest_id, blob_id) VALUES (?, ?)`,
		manifestID, blobID,
	)
	if err != nil {
		t.Fatalf("insert manifest_blob: %v", err)
	}

	return repoID
}

func TestGetRepositoryByName_Found(t *testing.T) {
	d := openTestDB(t)
	if err := d.Migrate(); err != nil {
		t.Fatalf("Migrate: %v", err)
	}

	seedTestRepo(t, d)

	id, err := d.GetRepositoryByName("myorg/myapp")
	if err != nil {
		t.Fatalf("GetRepositoryByName: %v", err)
	}
	if id == 0 {
		t.Fatal("expected non-zero repository ID")
	}
}

func TestGetRepositoryByName_NotFound(t *testing.T) {
	d := openTestDB(t)
	if err := d.Migrate(); err != nil {
		t.Fatalf("Migrate: %v", err)
	}

	_, err := d.GetRepositoryByName("nonexistent")
	if !errors.Is(err, ErrRepoNotFound) {
		t.Fatalf("expected ErrRepoNotFound, got %v", err)
	}
}

func TestGetManifestByTag_Found(t *testing.T) {
	d := openTestDB(t)
	if err := d.Migrate(); err != nil {
		t.Fatalf("Migrate: %v", err)
	}

	repoID := seedTestRepo(t, d)

	m, err := d.GetManifestByTag(repoID, "latest")
	if err != nil {
		t.Fatalf("GetManifestByTag: %v", err)
	}
	if m.Digest != "sha256:aaaa" {
		t.Fatalf("digest: got %q, want %q", m.Digest, "sha256:aaaa")
	}
	if m.MediaType != "application/vnd.oci.image.manifest.v1+json" {
		t.Fatalf("media type: got %q, want OCI manifest", m.MediaType)
	}
	if m.Size != 15 {
		t.Fatalf("size: got %d, want 15", m.Size)
	}
	if string(m.Content) != `{"layers":[]}` {
		t.Fatalf("content: got %q, want {\"layers\":[]}", string(m.Content))
	}
}

func TestGetManifestByTag_NotFound(t *testing.T) {
	d := openTestDB(t)
	if err := d.Migrate(); err != nil {
		t.Fatalf("Migrate: %v", err)
	}

	repoID := seedTestRepo(t, d)

	_, err := d.GetManifestByTag(repoID, "v0.0.0-nonexistent")
	if !errors.Is(err, ErrManifestNotFound) {
		t.Fatalf("expected ErrManifestNotFound, got %v", err)
	}
}

func TestGetManifestByDigest_Found(t *testing.T) {
	d := openTestDB(t)
	if err := d.Migrate(); err != nil {
		t.Fatalf("Migrate: %v", err)
	}

	repoID := seedTestRepo(t, d)

	m, err := d.GetManifestByDigest(repoID, "sha256:aaaa")
	if err != nil {
		t.Fatalf("GetManifestByDigest: %v", err)
	}
	if m.Digest != "sha256:aaaa" {
		t.Fatalf("digest: got %q, want %q", m.Digest, "sha256:aaaa")
	}
	if m.RepositoryID != repoID {
		t.Fatalf("repository_id: got %d, want %d", m.RepositoryID, repoID)
	}
}

func TestGetManifestByDigest_NotFound(t *testing.T) {
	d := openTestDB(t)
	if err := d.Migrate(); err != nil {
		t.Fatalf("Migrate: %v", err)
	}

	repoID := seedTestRepo(t, d)

	_, err := d.GetManifestByDigest(repoID, "sha256:nonexistent")
	if !errors.Is(err, ErrManifestNotFound) {
		t.Fatalf("expected ErrManifestNotFound, got %v", err)
	}
}

func TestBlobExistsInRepo_Exists(t *testing.T) {
	d := openTestDB(t)
	if err := d.Migrate(); err != nil {
		t.Fatalf("Migrate: %v", err)
	}

	repoID := seedTestRepo(t, d)

	exists, err := d.BlobExistsInRepo(repoID, "sha256:bbbb")
	if err != nil {
		t.Fatalf("BlobExistsInRepo: %v", err)
	}
	if !exists {
		t.Fatal("expected blob to exist in repo")
	}
}

func TestBlobExistsInRepo_NotInThisRepo(t *testing.T) {
	d := openTestDB(t)
	if err := d.Migrate(); err != nil {
		t.Fatalf("Migrate: %v", err)
	}

	seedTestRepo(t, d) // creates blob sha256:bbbb in myorg/myapp

	// Create a second repo with no manifests linking the blob.
	_, err := d.Exec(`INSERT INTO repositories (name) VALUES ('other/repo')`)
	if err != nil {
		t.Fatalf("insert other repo: %v", err)
	}
	var otherRepoID int64
	if err := d.QueryRow(`SELECT id FROM repositories WHERE name = 'other/repo'`).Scan(&otherRepoID); err != nil {
		t.Fatalf("select other repo id: %v", err)
	}

	exists, err := d.BlobExistsInRepo(otherRepoID, "sha256:bbbb")
	if err != nil {
		t.Fatalf("BlobExistsInRepo: %v", err)
	}
	if exists {
		t.Fatal("expected blob to NOT exist in other repo")
	}
}

func TestBlobExistsInRepo_BlobDoesNotExist(t *testing.T) {
	d := openTestDB(t)
	if err := d.Migrate(); err != nil {
		t.Fatalf("Migrate: %v", err)
	}

	repoID := seedTestRepo(t, d)

	exists, err := d.BlobExistsInRepo(repoID, "sha256:nonexistent")
	if err != nil {
		t.Fatalf("BlobExistsInRepo: %v", err)
	}
	if exists {
		t.Fatal("expected blob to not exist")
	}
}

func TestListTags_WithTags(t *testing.T) {
	d := openTestDB(t)
	if err := d.Migrate(); err != nil {
		t.Fatalf("Migrate: %v", err)
	}

	repoID := seedTestRepo(t, d)

	// Add more tags pointing to the same manifest.
	var manifestID int64
	if err := d.QueryRow(`SELECT id FROM manifests WHERE repository_id = ?`, repoID).Scan(&manifestID); err != nil {
		t.Fatalf("select manifest id: %v", err)
	}

	for _, tag := range []string{"v1.0", "v2.0", "beta"} {
		_, err := d.Exec(`INSERT INTO tags (repository_id, name, manifest_id) VALUES (?, ?, ?)`,
			repoID, tag, manifestID)
		if err != nil {
			t.Fatalf("insert tag %q: %v", tag, err)
		}
	}

	tags, err := d.ListTags(repoID, "", 100)
	if err != nil {
		t.Fatalf("ListTags: %v", err)
	}

	// Expect alphabetical: beta, latest, v1.0, v2.0
	want := []string{"beta", "latest", "v1.0", "v2.0"}
	if len(tags) != len(want) {
		t.Fatalf("tags count: got %d, want %d", len(tags), len(want))
	}
	for i, tag := range tags {
		if tag != want[i] {
			t.Fatalf("tags[%d]: got %q, want %q", i, tag, want[i])
		}
	}
}

func TestListTags_Pagination(t *testing.T) {
	d := openTestDB(t)
	if err := d.Migrate(); err != nil {
		t.Fatalf("Migrate: %v", err)
	}

	repoID := seedTestRepo(t, d)

	var manifestID int64
	if err := d.QueryRow(`SELECT id FROM manifests WHERE repository_id = ?`, repoID).Scan(&manifestID); err != nil {
		t.Fatalf("select manifest id: %v", err)
	}

	for _, tag := range []string{"v1.0", "v2.0", "beta"} {
		_, err := d.Exec(`INSERT INTO tags (repository_id, name, manifest_id) VALUES (?, ?, ?)`,
			repoID, tag, manifestID)
		if err != nil {
			t.Fatalf("insert tag %q: %v", tag, err)
		}
	}

	// First page: 2 tags starting from beginning.
	tags, err := d.ListTags(repoID, "", 2)
	if err != nil {
		t.Fatalf("ListTags page 1: %v", err)
	}
	if len(tags) != 2 {
		t.Fatalf("page 1 count: got %d, want 2", len(tags))
	}
	if tags[0] != "beta" || tags[1] != "latest" {
		t.Fatalf("page 1: got %v, want [beta, latest]", tags)
	}

	// Second page: after "latest".
	tags, err = d.ListTags(repoID, "latest", 2)
	if err != nil {
		t.Fatalf("ListTags page 2: %v", err)
	}
	if len(tags) != 2 {
		t.Fatalf("page 2 count: got %d, want 2", len(tags))
	}
	if tags[0] != "v1.0" || tags[1] != "v2.0" {
		t.Fatalf("page 2: got %v, want [v1.0, v2.0]", tags)
	}

	// Third page: after "v2.0" — no more tags.
	tags, err = d.ListTags(repoID, "v2.0", 2)
	if err != nil {
		t.Fatalf("ListTags page 3: %v", err)
	}
	if len(tags) != 0 {
		t.Fatalf("page 3 count: got %d, want 0", len(tags))
	}
}

func TestListTags_Empty(t *testing.T) {
	d := openTestDB(t)
	if err := d.Migrate(); err != nil {
		t.Fatalf("Migrate: %v", err)
	}

	// Create a repo with no tags.
	_, err := d.Exec(`INSERT INTO repositories (name) VALUES ('empty/repo')`)
	if err != nil {
		t.Fatalf("insert repo: %v", err)
	}
	var repoID int64
	if err := d.QueryRow(`SELECT id FROM repositories WHERE name = 'empty/repo'`).Scan(&repoID); err != nil {
		t.Fatalf("select repo id: %v", err)
	}

	tags, err := d.ListTags(repoID, "", 100)
	if err != nil {
		t.Fatalf("ListTags: %v", err)
	}
	if tags != nil {
		t.Fatalf("expected nil tags, got %v", tags)
	}
}

func TestListRepositoryNames_WithRepos(t *testing.T) {
	d := openTestDB(t)
	if err := d.Migrate(); err != nil {
		t.Fatalf("Migrate: %v", err)
	}

	for _, name := range []string{"charlie/app", "alpha/lib", "bravo/svc"} {
		_, err := d.Exec(`INSERT INTO repositories (name) VALUES (?)`, name)
		if err != nil {
			t.Fatalf("insert repo %q: %v", name, err)
		}
	}

	names, err := d.ListRepositoryNames("", 100)
	if err != nil {
		t.Fatalf("ListRepositoryNames: %v", err)
	}

	want := []string{"alpha/lib", "bravo/svc", "charlie/app"}
	if len(names) != len(want) {
		t.Fatalf("names count: got %d, want %d", len(names), len(want))
	}
	for i, n := range names {
		if n != want[i] {
			t.Fatalf("names[%d]: got %q, want %q", i, n, want[i])
		}
	}
}

func TestListRepositoryNames_Pagination(t *testing.T) {
	d := openTestDB(t)
	if err := d.Migrate(); err != nil {
		t.Fatalf("Migrate: %v", err)
	}

	for _, name := range []string{"charlie/app", "alpha/lib", "bravo/svc"} {
		_, err := d.Exec(`INSERT INTO repositories (name) VALUES (?)`, name)
		if err != nil {
			t.Fatalf("insert repo %q: %v", name, err)
		}
	}

	// First page: 2.
	names, err := d.ListRepositoryNames("", 2)
	if err != nil {
		t.Fatalf("ListRepositoryNames page 1: %v", err)
	}
	if len(names) != 2 {
		t.Fatalf("page 1 count: got %d, want 2", len(names))
	}
	if names[0] != "alpha/lib" || names[1] != "bravo/svc" {
		t.Fatalf("page 1: got %v", names)
	}

	// Second page: after "bravo/svc".
	names, err = d.ListRepositoryNames("bravo/svc", 2)
	if err != nil {
		t.Fatalf("ListRepositoryNames page 2: %v", err)
	}
	if len(names) != 1 {
		t.Fatalf("page 2 count: got %d, want 1", len(names))
	}
	if names[0] != "charlie/app" {
		t.Fatalf("page 2: got %v", names)
	}
}

func TestListRepositoryNames_Empty(t *testing.T) {
	d := openTestDB(t)
	if err := d.Migrate(); err != nil {
		t.Fatalf("Migrate: %v", err)
	}

	names, err := d.ListRepositoryNames("", 100)
	if err != nil {
		t.Fatalf("ListRepositoryNames: %v", err)
	}
	if names != nil {
		t.Fatalf("expected nil names, got %v", names)
	}
}
