package auctiora;

import org.junit.jupiter.api.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

/**
 * Test cases for DatabaseService.
 * Tests database operations including schema creation, CRUD operations, and data retrieval.
 */
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DatabaseServiceTest {

    private DatabaseService db;
    private String testDbPath;

    @BeforeAll
    void setUp() throws SQLException {
        testDbPath = "test_database_" + System.currentTimeMillis() + ".db";
        db = new DatabaseService(testDbPath);
        db.ensureSchema();
    }

    @AfterAll
    void tearDown() throws Exception {
        // Clean up test database
        Files.deleteIfExists(Paths.get(testDbPath));
    }

    @Test
    @DisplayName("Should create database schema successfully")
    void testEnsureSchema() {
        assertDoesNotThrow(() -> db.ensureSchema());
    }

    @Test
    @DisplayName("Should insert and retrieve auction")
    void testUpsertAndGetAuction() throws SQLException {
        var auction = new AuctionInfo(
            12345,
            "Test Auction",
            "Amsterdam, NL",
            "Amsterdam",
            "NL",
            "https://example.com/auction/12345",
            "A7",
            50,
            LocalDateTime.of(2025, 12, 15, 14, 30)
        );

        db.upsertAuction(auction);

        var auctions = db.getAllAuctions();
        assertFalse(auctions.isEmpty());

        var retrieved = auctions.stream()
            .filter(a -> a.auctionId() == 12345)
            .findFirst()
            .orElse(null);

        assertNotNull(retrieved);
        assertEquals("Test Auction", retrieved.title());
        assertEquals("Amsterdam", retrieved.city());
        assertEquals("NL", retrieved.country());
        assertEquals(50, retrieved.lotCount());
    }

    @Test
    @DisplayName("Should update existing auction on conflict")
    void testUpsertAuctionUpdate() throws SQLException {
        var auction1 = new AuctionInfo(
            99999,
            "Original Title",
            "Rotterdam, NL",
            "Rotterdam",
            "NL",
            "https://example.com/auction/99999",
            "A1",
            10,
            null
        );

        db.upsertAuction(auction1);

        // Update with same ID
        var auction2 = new AuctionInfo(
            99999,
            "Updated Title",
            "Rotterdam, NL",
            "Rotterdam",
            "NL",
            "https://example.com/auction/99999",
            "A1",
            20,
            null
        );

        db.upsertAuction(auction2);

        var auctions = db.getAllAuctions();
        var retrieved = auctions.stream()
            .filter(a -> a.auctionId() == 99999)
            .findFirst()
            .orElse(null);

        assertNotNull(retrieved);
        assertEquals("Updated Title", retrieved.title());
        assertEquals(20, retrieved.lotCount());
    }

    @Test
    @DisplayName("Should retrieve auctions by country code")
    void testGetAuctionsByCountry() throws SQLException {
        // Insert auctions from different countries
        db.upsertAuction(new AuctionInfo(
            10001, "Dutch Auction", "Amsterdam, NL", "Amsterdam", "NL",
            "https://example.com/10001", "A1", 10, null
        ));

        db.upsertAuction(new AuctionInfo(
            10002, "Romanian Auction", "Cluj, RO", "Cluj", "RO",
            "https://example.com/10002", "A2", 15, null
        ));

        db.upsertAuction(new AuctionInfo(
            10003, "Another Dutch", "Utrecht, NL", "Utrecht", "NL",
            "https://example.com/10003", "A3", 20, null
        ));

        var nlAuctions = db.getAuctionsByCountry("NL");
        assertEquals(2, nlAuctions.stream().filter(a -> a.auctionId() >= 10001 && a.auctionId() <= 10003).count());

        var roAuctions = db.getAuctionsByCountry("RO");
        assertEquals(1, roAuctions.stream().filter(a -> a.auctionId() == 10002).count());
    }

    @Test
    @DisplayName("Should insert and retrieve lot")
    void testUpsertAndGetLot() throws SQLException {
        var lot = new Lot(
            12345,  // saleId
            67890,  // lotId
            "Forklift",
            "Electric forklift in good condition",
            "Toyota",
            "Electric",
            2018,
            "Machinery",
            1500.00,
            "EUR",
            "https://example.com/lot/67890",
            LocalDateTime.of(2025, 12, 20, 16, 0),
            false
        );

        db.upsertLot(lot);

        var lots = db.getAllLots();
        assertFalse(lots.isEmpty());

        var retrieved = lots.stream()
            .filter(l -> l.lotId() == 67890)
            .findFirst()
            .orElse(null);

        assertNotNull(retrieved);
        assertEquals("Forklift", retrieved.title());
        assertEquals("Toyota", retrieved.manufacturer());
        assertEquals(2018, retrieved.year());
        assertEquals(1500.00, retrieved.currentBid(), 0.01);
        assertFalse(retrieved.closingNotified());
    }

    @Test
    @DisplayName("Should update lot current bid")
    void testUpdateLotCurrentBid() throws SQLException {
        var lot = new Lot(
            11111, 22222, "Test Item", "Description", "", "", 0, "Category",
            100.00, "EUR", "https://example.com/lot/22222", null, false
        );

        db.upsertLot(lot);

        // Update bid
        var updatedLot = new Lot(
            11111, 22222, "Test Item", "Description", "", "", 0, "Category",
            250.00, "EUR", "https://example.com/lot/22222", null, false
        );

        db.updateLotCurrentBid(updatedLot);

        var lots = db.getAllLots();
        var retrieved = lots.stream()
            .filter(l -> l.lotId() == 22222)
            .findFirst()
            .orElse(null);

        assertNotNull(retrieved);
        assertEquals(250.00, retrieved.currentBid(), 0.01);
    }

    @Test
    @DisplayName("Should update lot notification flags")
    void testUpdateLotNotificationFlags() throws SQLException {
        var lot = new Lot(
            33333, 44444, "Test Item", "Description", "", "", 0, "Category",
            100.00, "EUR", "https://example.com/lot/44444", null, false
        );

        db.upsertLot(lot);

        // Update notification flag
        var updatedLot = new Lot(
            33333, 44444, "Test Item", "Description", "", "", 0, "Category",
            100.00, "EUR", "https://example.com/lot/44444", null, true
        );

        db.updateLotNotificationFlags(updatedLot);

        var lots = db.getAllLots();
        var retrieved = lots.stream()
            .filter(l -> l.lotId() == 44444)
            .findFirst()
            .orElse(null);

        assertNotNull(retrieved);
        assertTrue(retrieved.closingNotified());
    }

    @Test
    @DisplayName("Should insert and retrieve image records")
    void testInsertAndGetImages() throws SQLException {
        // First create a lot
        var lot = new Lot(
            55555, 66666, "Test Lot", "Description", "", "", 0, "Category",
            100.00, "EUR", "https://example.com/lot/66666", null, false
        );
        db.upsertLot(lot);

        // Insert images
        db.insertImage(66666, "https://example.com/img1.jpg",
            "C:/images/66666/img1.jpg", List.of("car", "vehicle"));

        db.insertImage(66666, "https://example.com/img2.jpg",
            "C:/images/66666/img2.jpg", List.of("truck"));

        var images = db.getImagesForLot(66666);
        assertEquals(2, images.size());

        var img1 = images.stream()
            .filter(i -> i.url().contains("img1.jpg"))
            .findFirst()
            .orElse(null);

        assertNotNull(img1);
        assertEquals("car,vehicle", img1.labels());
    }

    @Test
    @DisplayName("Should count total images")
    void testGetImageCount() throws SQLException {
        int initialCount = db.getImageCount();

        // Add a lot and image
        var lot = new Lot(
            77777, 88888, "Test Lot", "Description", "", "", 0, "Category",
            100.00, "EUR", "https://example.com/lot/88888", null, false
        );
        db.upsertLot(lot);

        db.insertImage(88888, "https://example.com/test.jpg",
            "C:/images/88888/test.jpg", List.of("object"));

        int newCount = db.getImageCount();
        assertTrue(newCount > initialCount);
    }

    @Test
    @DisplayName("Should handle empty database gracefully")
    void testEmptyDatabase() throws SQLException, IOException {
        DatabaseService emptyDb = new DatabaseService("empty_test_" + System.currentTimeMillis() + ".db");
        emptyDb.ensureSchema();

        var auctions = emptyDb.getAllAuctions();
        var lots = emptyDb.getAllLots();
        int imageCount = emptyDb.getImageCount();

        assertNotNull(auctions);
        assertNotNull(lots);
        assertTrue(auctions.isEmpty());
        assertTrue(lots.isEmpty());
        assertEquals(0, imageCount);

        // Clean up
        Files.deleteIfExists(Paths.get("empty_test_" + System.currentTimeMillis() + ".db"));
    }

    @Test
    @DisplayName("Should handle lots with null closing time")
    void testLotWithNullClosingTime() throws SQLException {
        var lot = new Lot(
            98765, 12340, "Test Item", "Description", "", "", 0, "Category",
            100.00, "EUR", "https://example.com/lot/12340", null, false
        );

        assertDoesNotThrow(() -> db.upsertLot(lot));

        var retrieved = db.getAllLots().stream()
            .filter(l -> l.lotId() == 12340)
            .findFirst()
            .orElse(null);

        assertNotNull(retrieved);
        assertNull(retrieved.closingTime());
    }

    @Test
    @DisplayName("Should retrieve active lots only")
    void testGetActiveLots() throws SQLException {
        var activeLot = new Lot(
            11111, 55551, "Active Lot", "Description", "", "", 0, "Category",
            100.00, "EUR", "https://example.com/lot/55551",
            LocalDateTime.now().plusDays(1), false
        );

        db.upsertLot(activeLot);

        var activeLots = db.getActiveLots();
        assertFalse(activeLots.isEmpty());

        var found = activeLots.stream()
            .anyMatch(l -> l.lotId() == 55551);
        assertTrue(found);
    }

    @Test
    @DisplayName("Should handle concurrent upserts")
    void testConcurrentUpserts() throws InterruptedException, SQLException {
        Thread t1 = new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    db.upsertLot(new Lot(
                        99000 + i, 99100 + i, "Concurrent " + i, "Desc", "", "", 0, "Cat",
                        100.0, "EUR", "https://example.com/" + i, null, false
                    ));
                }
            } catch (SQLException e) {
                fail("Thread 1 failed: " + e.getMessage());
            }
        });

        Thread t2 = new Thread(() -> {
            try {
                for (int i = 10; i < 20; i++) {
                    db.upsertLot(new Lot(
                        99000 + i, 99100 + i, "Concurrent " + i, "Desc", "", "", 0, "Cat",
                        200.0, "EUR", "https://example.com/" + i, null, false
                    ));
                }
            } catch (SQLException e) {
                fail("Thread 2 failed: " + e.getMessage());
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        var lots = db.getAllLots();
        long concurrentLots = lots.stream()
            .filter(l -> l.lotId() >= 99100 && l.lotId() < 99120)
            .count();

        assertTrue(concurrentLots >= 20);
    }
}
