package app

import (
	"context"
	"errors"
	"testing"

	"evening_detective_server/internal/modules/file_storage"
	"evening_detective_server/internal/modules/processor_jwt"
	"evening_detective_server/internal/modules/string_tools"
	"evening_detective_server/internal/services/file_service"
	proto "evening_detective_server/proto"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
)

// fakeStorage — in-memory реализация IFileStorage для app-слоя.
type fakeStorage struct {
	files  map[string]*file_storage.File
	putErr error
	getErr error
}

func newFakeStorage() *fakeStorage {
	return &fakeStorage{files: map[string]*file_storage.File{}}
}

func (s *fakeStorage) Put(_ context.Context, f *file_storage.File) error {
	if s.putErr != nil {
		return s.putErr
	}
	cp := *f
	cp.Data = append([]byte(nil), f.Data...)
	s.files[f.Name] = &cp
	return nil
}

func (s *fakeStorage) Get(_ context.Context, name string) (*file_storage.File, error) {
	if s.getErr != nil {
		return nil, s.getErr
	}
	f, ok := s.files[name]
	if !ok {
		return nil, file_storage.ErrFileNotFound
	}
	return f, nil
}

func (s *fakeStorage) Delete(_ context.Context, name string) error {
	delete(s.files, name)
	return nil
}

func (s *fakeStorage) MimeType(filename string) string {
	return file_storage.FileType(filename)
}

func newTestFileServer(storage *fakeStorage, maxSize int) *server {
	if storage == nil {
		storage = newFakeStorage()
	}
	return &server{
		fileService: file_service.NewFileService(storage, string_tools.NewStringTools(), maxSize),
	}
}

func ctxWithClaims(roles ...string) context.Context {
	return context.WithValue(context.Background(), "claims", &processor_jwt.JWTClaims{UserID: 1, Roles: roles})
}

func pdfData(n int) []byte {
	b := make([]byte, n)
	copy(b, "%PDF-1.7 ")
	for i := 8; i < n; i++ {
		b[i] = 'x'
	}
	return b
}

func TestUploadFilePermissionDenied(t *testing.T) {
	s := newTestFileServer(nil, 1<<20)
	_, err := s.UploadFile(ctxWithClaims(), &proto.UploadFileReq{Filename: "clue.pdf", Data: pdfData(16)})
	if status.Code(err) != codes.PermissionDenied {
		t.Errorf("code = %v, want PermissionDenied", status.Code(err))
	}
}

func TestUploadFileValidationInvalidArgument(t *testing.T) {
	s := newTestFileServer(nil, 1<<20)
	// Имя без расширения.
	_, err := s.UploadFile(ctxWithClaims("author"), &proto.UploadFileReq{Filename: "clue", Data: pdfData(16)})
	if status.Code(err) != codes.InvalidArgument {
		t.Errorf("code = %v, want InvalidArgument", status.Code(err))
	}
}

func TestUploadFileTooLargeResourceExhausted(t *testing.T) {
	s := newTestFileServer(nil, 1024)
	_, err := s.UploadFile(ctxWithClaims("author"), &proto.UploadFileReq{Filename: "clue.pdf", Data: pdfData(1025)})
	if status.Code(err) != codes.ResourceExhausted {
		t.Errorf("code = %v, want ResourceExhausted", status.Code(err))
	}
}

func TestUploadFilePutErrorInternal(t *testing.T) {
	storage := newFakeStorage()
	storage.putErr = errors.New("s3 недоступен")
	s := newTestFileServer(storage, 1<<20)

	_, err := s.UploadFile(ctxWithClaims("author"), &proto.UploadFileReq{Filename: "clue.pdf", Data: pdfData(16)})
	if status.Code(err) != codes.Internal {
		t.Errorf("code = %v, want Internal", status.Code(err))
	}
}

func TestUploadFileSuccess(t *testing.T) {
	s := newTestFileServer(nil, 1<<20)
	rsp, err := s.UploadFile(ctxWithClaims("author"), &proto.UploadFileReq{Filename: "clue.pdf", Data: pdfData(16)})
	if err != nil {
		t.Fatalf("UploadFile: %v", err)
	}
	if rsp.Filename == "" {
		t.Errorf("Filename пустой")
	}
	if rsp.FileType != "pdf" {
		t.Errorf("FileType = %q, want pdf", rsp.FileType)
	}
	if rsp.Error != "" {
		t.Errorf("Error = %q, want пусто", rsp.Error)
	}
}

// mockServerTransportStream — заглушка для grpc.SetHeader в юнит-тестах.
type mockServerTransportStream struct {
	grpc.ServerTransportStream
	header metadata.MD
}

func (m *mockServerTransportStream) SetHeader(md metadata.MD) error {
	m.header = md
	return nil
}

func TestDownloadFileNotFound(t *testing.T) {
	s := newTestFileServer(nil, 1<<20)
	_, err := s.DownloadFile(context.Background(), &proto.DownloadFileReq{Filename: "missing.pdf"})
	if status.Code(err) != codes.NotFound {
		t.Errorf("code = %v, want NotFound", status.Code(err))
	}
}

func TestDownloadFileInternal(t *testing.T) {
	storage := newFakeStorage()
	storage.getErr = errors.New("s3 недоступен")
	s := newTestFileServer(storage, 1<<20)

	_, err := s.DownloadFile(context.Background(), &proto.DownloadFileReq{Filename: "clue.pdf"})
	if status.Code(err) != codes.Internal {
		t.Errorf("code = %v, want Internal", status.Code(err))
	}
}

func TestDownloadFileSuccess(t *testing.T) {
	storage := newFakeStorage()
	storage.files["clue.pdf"] = &file_storage.File{
		Name: "clue.pdf",
		Data: []byte("%PDF-1.7 content"),
		Mime: "application/pdf",
	}
	s := newTestFileServer(storage, 1<<20)

	stream := &mockServerTransportStream{}
	ctx := grpc.NewContextWithServerTransportStream(context.Background(), stream)

	body, err := s.DownloadFile(ctx, &proto.DownloadFileReq{Filename: "clue.pdf"})
	if err != nil {
		t.Fatalf("DownloadFile: %v", err)
	}
	if string(body.Data) != "%PDF-1.7 content" {
		t.Errorf("Data = %q", body.Data)
	}
	if body.ContentType != "application/pdf" {
		t.Errorf("ContentType = %q, want application/pdf", body.ContentType)
	}
	if got := stream.header.Get("X-Content-Type-Options"); len(got) != 1 || got[0] != "nosniff" {
		t.Errorf("X-Content-Type-Options = %v, want nosniff", got)
	}
}
