package datastore

import (
	"context"
	"database/sql"
	"errors"
	"strings"
	"testing"
	"time"
)

func TestStatAppendLogTargetNotFound(t *testing.T) {
	s := newTestStore(t)
	_, err := s.StatAppendLogTarget(context.Background(), "/missing")
	if !errors.Is(err, ErrNotFound) {
		t.Fatalf("missing dentry error = %v, want ErrNotFound", err)
	}
}

func TestStatAppendLogTargetDirectory(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	now := time.Now().UTC()
	if err := s.InsertNodeTx(s.DB(), dirTestNode("/dir/", now)); err != nil {
		t.Fatalf("insert directory: %v", err)
	}
	nf, err := s.StatAppendLogTarget(ctx, "/dir/")
	if err != nil {
		t.Fatalf("stat directory: %v", err)
	}
	if !nf.Node.IsDirectory {
		t.Fatalf("node is_directory = %v, want true", nf.Node.IsDirectory)
	}
	if nf.File != nil {
		t.Fatalf("directory File = %+v, want nil", nf.File)
	}
}

func TestStatAppendLogTargetAppendLogFile(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	now := time.Now().UTC()
	if err := s.InTx(ctx, func(tx *sql.Tx) error {
		if err := s.InsertFileTx(tx, &File{
			FileID:        "append-log-id",
			StorageType:   StorageS3,
			ContentLayout: ContentLayoutAppendLog,
			StorageRef:    "append-log/ns-a/id-a/generation-a",
			ContentType:   "application/octet-stream",
			SizeBytes:     4,
			Revision:      2,
			Status:        StatusConfirmed,
			CreatedAt:     now,
			ConfirmedAt:   &now,
		}); err != nil {
			return err
		}
		return s.InsertNodeTx(tx, fileTestNode("/wal", "append-log-id", now))
	}); err != nil {
		t.Fatalf("insert append-log file: %v", err)
	}
	nf, err := s.StatAppendLogTarget(ctx, "/wal")
	if err != nil {
		t.Fatalf("stat append-log file: %v", err)
	}
	if nf.Node.IsDirectory || nf.File == nil {
		t.Fatalf("node = %+v, want file with File", nf)
	}
	f := nf.File
	if f.FileID != "append-log-id" || f.StorageType != StorageS3 || f.ContentLayout != ContentLayoutAppendLog ||
		f.StorageRef != "append-log/ns-a/id-a/generation-a" || f.ContentType != "application/octet-stream" ||
		f.SizeBytes != 4 || f.Revision != 2 {
		t.Fatalf("narrow file = %+v, want append-log identity fields", f)
	}
}

func TestStatAppendLogTargetDanglingDentry(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	now := time.Now().UTC()
	// A file dentry whose inode row is absent must mirror Stat: GetFile
	// propagates GetInode's sql.ErrNoRows as ErrNotFound.
	if _, err := s.DB().ExecContext(ctx, `INSERT INTO file_nodes (`+s.scope.InsCols(`node_id, path, path_hash, parent_path, parent_path_hash, name, is_directory, file_id, inode_id, created_at`)+`)
		VALUES (`+s.scope.InsVals(`?, ?, ?, ?, ?, ?, ?, ?, ?, ?`)+`)`,
		s.scope.Args("dangling-node", "/dangling", fileNodePathHash("/dangling"), "/",
			fileNodePathHash("/"), "dangling", false, "ghost-inode", nil, now.UTC())...); err != nil {
		t.Fatalf("insert dangling dentry: %v", err)
	}
	_, err := s.StatAppendLogTarget(ctx, "/dangling")
	if !errors.Is(err, ErrNotFound) {
		t.Fatalf("dangling dentry error = %v, want ErrNotFound", err)
	}
}

func TestStatAppendLogTargetMissingContents(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	now := time.Now().UTC()
	if err := s.InTx(ctx, func(tx *sql.Tx) error {
		if err := s.InsertFileTx(tx, &File{
			FileID:        "inode-no-contents",
			StorageType:   StorageS3,
			ContentLayout: ContentLayoutAppendLog,
			StorageRef:    "append-log/ns-a/id-a/generation-missing",
			SizeBytes:     8,
			Revision:      3,
			Status:        StatusConfirmed,
			CreatedAt:     now,
			ConfirmedAt:   &now,
		}); err != nil {
			return err
		}
		return s.InsertNodeTx(tx, fileTestNode("/nocontents", "inode-no-contents", now))
	}); err != nil {
		t.Fatalf("insert append-log file: %v", err)
	}
	if _, err := s.DB().ExecContext(ctx, `DELETE FROM contents WHERE `+s.scope.And(`inode_id = ?`), s.scope.Args("inode-no-contents")...); err != nil {
		t.Fatalf("delete contents row: %v", err)
	}
	if _, err := s.StatAppendLogTarget(ctx, "/nocontents"); !errors.Is(err, ErrNotFound) {
		t.Fatalf("stat inode without contents: err = %v, want ErrNotFound", err)
	}
}

func TestStatAppendLogTargetMissingSemantic(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	now := time.Now().UTC()
	if err := s.InTx(ctx, func(tx *sql.Tx) error {
		if err := s.InsertFileTx(tx, &File{
			FileID:        "inode-no-semantic",
			StorageType:   StorageS3,
			ContentLayout: ContentLayoutAppendLog,
			StorageRef:    "append-log/ns-a/id-a/generation-no-semantic",
			SizeBytes:     8,
			Revision:      3,
			Status:        StatusConfirmed,
			CreatedAt:     now,
			ConfirmedAt:   &now,
		}); err != nil {
			return err
		}
		return s.InsertNodeTx(tx, fileTestNode("/nosemantic", "inode-no-semantic", now))
	}); err != nil {
		t.Fatalf("insert append-log file: %v", err)
	}
	if _, err := s.DB().ExecContext(ctx, `DELETE FROM semantic WHERE `+s.scope.And(`inode_id = ?`), s.scope.Args("inode-no-semantic")...); err != nil {
		t.Fatalf("delete semantic row: %v", err)
	}
	if _, err := s.StatAppendLogTarget(ctx, "/nosemantic"); !errors.Is(err, ErrNotFound) {
		t.Fatalf("stat inode without semantic: err = %v, want ErrNotFound", err)
	}
}

func TestStatAppendLogTargetNonConfirmedInode(t *testing.T) {
	for _, status := range []FileStatus{StatusPending, StatusDeleted} {
		t.Run(string(status), func(t *testing.T) {
			s := newTestStore(t)
			ctx := context.Background()
			now := time.Now().UTC()
			id := "inode-" + string(status)
			ref := "append-log/ns-a/id-a/generation-" + string(status)
			if err := s.InTx(ctx, func(tx *sql.Tx) error {
				if err := s.InsertFileTx(tx, &File{
					FileID:        id,
					StorageType:   StorageS3,
					ContentLayout: ContentLayoutAppendLog,
					StorageRef:    ref,
					SizeBytes:     8,
					Revision:      3,
					Status:        StatusConfirmed,
					CreatedAt:     now,
					ConfirmedAt:   &now,
				}); err != nil {
					return err
				}
				return s.InsertNodeTx(tx, fileTestNode("/"+id, id, now))
			}); err != nil {
				t.Fatalf("insert append-log file: %v", err)
			}
			if _, err := s.DB().ExecContext(ctx, `UPDATE inodes SET status = ? WHERE `+s.scope.And(`inode_id = ?`),
				append([]any{status}, s.scope.Args(id)...)...); err != nil {
				t.Fatalf("set inode status: %v", err)
			}
			if _, err := s.StatAppendLogTarget(ctx, "/"+id); !errors.Is(err, ErrNotFound) {
				t.Fatalf("stat %s inode: err = %v, want ErrNotFound", status, err)
			}
		})
	}
}

func TestAppendLogContentIfMatchTxClearsContentsChecksum(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	now := time.Now().UTC()
	const fileID = "append-log-checksum"
	const ref = "append-log/ns-a/id-a/generation-checksum"
	if err := s.InTx(ctx, func(tx *sql.Tx) error {
		if err := s.InsertFileTx(tx, &File{
			FileID:        fileID,
			StorageType:   StorageS3,
			ContentLayout: ContentLayoutAppendLog,
			StorageRef:    ref,
			SizeBytes:     4,
			Revision:      2,
			Status:        StatusConfirmed,
			CreatedAt:     now,
			ConfirmedAt:   &now,
		}); err != nil {
			return err
		}
		return s.InsertNodeTx(tx, fileTestNode("/wal-checksum", fileID, now))
	}); err != nil {
		t.Fatalf("insert append-log file: %v", err)
	}

	// Simulate repair/tooling/backfill having persisted checksum and blob
	// metadata on the append-log contents row before this append.
	if _, err := s.DB().ExecContext(ctx, `UPDATE contents SET checksum_sha256 = ?, content_blob = ?
		WHERE `+s.scope.And(`inode_id = ? AND storage_type = ? AND content_layout = ? AND storage_ref = ?`),
		append([]any{strings.Repeat("a", 64), []byte("stale-blob")}, s.scope.Args(fileID, StorageS3, ContentLayoutAppendLog, ref)...)...); err != nil {
		t.Fatalf("seed contents checksum: %v", err)
	}

	live, err := s.StatAppendLogTarget(ctx, "/wal-checksum")
	if err != nil {
		t.Fatalf("stat append-log target: %v", err)
	}
	var revision int64
	if err := s.InTx(ctx, func(tx *sql.Tx) error {
		rev, err := s.AppendLogContentIfMatchTx(tx, live, 2, 4, ref, 3)
		if err != nil {
			return err
		}
		revision = rev
		return nil
	}); err != nil {
		t.Fatalf("append content if match: %v", err)
	}
	if revision != 3 {
		t.Fatalf("revision = %d, want 3", revision)
	}

	content, err := s.GetContent(ctx, fileID)
	if err != nil {
		t.Fatalf("get content: %v", err)
	}
	if content.ChecksumSHA256 != "" {
		t.Fatalf("contents.checksum_sha256 = %q, want cleared", content.ChecksumSHA256)
	}
	if len(content.ContentBlob) != 0 {
		t.Fatalf("contents.content_blob = %d bytes, want cleared", len(content.ContentBlob))
	}
}
