package com.livingworld.data.migration;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;

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

class MigrationManagerTest {

    // ------------------------------------------------------------------
    // helpers
    // ------------------------------------------------------------------

    private static Properties propsAtVersion(int version) {
        Properties p = new Properties();
        p.setProperty("schemaVersion", Integer.toString(version));
        p.setProperty("someField", "original");
        return p;
    }

    /** Migration that adds a tag proving it ran. */
    private static RegionMigration tagMigration(int from) {
        return new RegionMigration() {
            @Override public int fromVersion() { return from; }
            @Override public int toVersion()   { return from + 1; }
            @Override public Properties apply(Properties data) {
                Properties out = new Properties();
                out.putAll(data);
                out.setProperty("migrated_" + from + "_to_" + (from + 1), "true");
                return out;
            }
        };
    }

    // ------------------------------------------------------------------
    // construction
    // ------------------------------------------------------------------

    @Test
    void newManagerHasNoMigrations() {
        MigrationManager manager = new MigrationManager(null);
        assertEquals(0, manager.getRegisteredMigrationCount());
    }

    // ------------------------------------------------------------------
    // registration
    // ------------------------------------------------------------------

    @Test
    void registerNullMigrationThrows() {
        MigrationManager manager = new MigrationManager(null);
        assertThrows(IllegalArgumentException.class, () -> manager.register(null));
    }

    @Test
    void registerMigrationThatSkipsVersionThrows() {
        MigrationManager manager = new MigrationManager(null);
        RegionMigration bad = new RegionMigration() {
            @Override public int fromVersion() { return 1; }
            @Override public int toVersion()   { return 3; } // skips v2
            @Override public Properties apply(Properties data) { return data; }
        };
        assertThrows(IllegalArgumentException.class, () -> manager.register(bad));
    }

    @Test
    void registerDuplicateFromVersionThrows() {
        MigrationManager manager = new MigrationManager(null);
        manager.register(tagMigration(1));
        assertThrows(IllegalArgumentException.class, () -> manager.register(tagMigration(1)));
    }

    @Test
    void registerIncreasesCount() {
        MigrationManager manager = new MigrationManager(null);
        manager.register(tagMigration(1));
        manager.register(tagMigration(2));
        assertEquals(2, manager.getRegisteredMigrationCount());
    }

    // ------------------------------------------------------------------
    // isUpToDate
    // ------------------------------------------------------------------

    @Test
    void isUpToDateReturnsTrueWhenVersionMatches() {
        MigrationManager manager = new MigrationManager(null);
        assertTrue(manager.isUpToDate(propsAtVersion(1), 1));
    }

    @Test
    void isUpToDateReturnsFalseWhenBehind() {
        MigrationManager manager = new MigrationManager(null);
        assertFalse(manager.isUpToDate(propsAtVersion(1), 2));
    }

    // ------------------------------------------------------------------
    // migrateIfNeeded — already at target
    // ------------------------------------------------------------------

    @Test
    void migrateIfNeededReturnsSameObjectWhenAlreadyAtTarget() {
        MigrationManager manager = new MigrationManager(null);
        Properties data = propsAtVersion(1);
        Properties result = manager.migrateIfNeeded(data, 1);
        assertSame(data, result);
    }

    // ------------------------------------------------------------------
    // migrateIfNeeded — single step
    // ------------------------------------------------------------------

    @Test
    void singleMigrationIsApplied() {
        MigrationManager manager = new MigrationManager(null);
        manager.register(tagMigration(1));

        Properties data = propsAtVersion(1);
        Properties result = manager.migrateIfNeeded(data, 2);

        assertEquals("2", result.getProperty("schemaVersion"));
        assertEquals("true", result.getProperty("migrated_1_to_2"));
        assertEquals("original", result.getProperty("someField")); // original data preserved
    }

    // ------------------------------------------------------------------
    // migrateIfNeeded — multi-step
    // ------------------------------------------------------------------

    @Test
    void multiStepMigrationAppliesAllStepsInOrder() {
        MigrationManager manager = new MigrationManager(null);
        manager.register(tagMigration(1));
        manager.register(tagMigration(2));

        Properties result = manager.migrateIfNeeded(propsAtVersion(1), 3);

        assertEquals("3", result.getProperty("schemaVersion"));
        assertEquals("true", result.getProperty("migrated_1_to_2"));
        assertEquals("true", result.getProperty("migrated_2_to_3"));
    }

    // ------------------------------------------------------------------
    // migrateIfNeeded — source data is not mutated
    // ------------------------------------------------------------------

    @Test
    void originalDataIsNotMutatedByMigration() {
        MigrationManager manager = new MigrationManager(null);
        manager.register(tagMigration(1));

        Properties data = propsAtVersion(1);
        manager.migrateIfNeeded(data, 2);

        assertEquals("1", data.getProperty("schemaVersion"));
        assertNull(data.getProperty("migrated_1_to_2"));
    }

    // ------------------------------------------------------------------
    // migrateIfNeeded — error cases
    // ------------------------------------------------------------------

    @Test
    void downgradeAttemptThrows() {
        MigrationManager manager = new MigrationManager(null);
        assertThrows(IllegalStateException.class,
                () -> manager.migrateIfNeeded(propsAtVersion(2), 1));
    }

    @Test
    void missingMigrationInChainThrows() {
        MigrationManager manager = new MigrationManager(null);
        manager.register(tagMigration(1));
        // no migration for version 2 → 3

        // data at v1, target v3: step 1→2 exists but 2→3 is missing
        assertThrows(IllegalStateException.class,
                () -> manager.migrateIfNeeded(propsAtVersion(1), 3));
    }

    @Test
    void missingSchemaVersionKeyThrows() {
        MigrationManager manager = new MigrationManager(null);
        Properties data = new Properties();
        data.setProperty("someField", "value");
        assertThrows(IllegalStateException.class,
                () -> manager.migrateIfNeeded(data, 2));
    }

    @Test
    void invalidSchemaVersionValueThrows() {
        MigrationManager manager = new MigrationManager(null);
        Properties data = new Properties();
        data.setProperty("schemaVersion", "not-a-number");
        assertThrows(IllegalStateException.class,
                () -> manager.migrateIfNeeded(data, 2));
    }

    @Test
    void zeroSchemaVersionThrows() {
        MigrationManager manager = new MigrationManager(null);
        Properties data = new Properties();
        data.setProperty("schemaVersion", "0");
        assertThrows(IllegalStateException.class,
                () -> manager.migrateIfNeeded(data, 1));
    }

    @Test
    void nullDataThrows() {
        MigrationManager manager = new MigrationManager(null);
        assertThrows(IllegalArgumentException.class,
                () -> manager.migrateIfNeeded(null, 1));
    }

    // ------------------------------------------------------------------
    // migrations.log
    // ------------------------------------------------------------------

    @Test
    void migrationIsRecordedInLogFile(@TempDir Path tempDir) throws Exception {
        Path logFile = tempDir.resolve("migrations.log");
        MigrationManager manager = new MigrationManager(logFile);
        manager.register(tagMigration(1));

        manager.migrateIfNeeded(propsAtVersion(1), 2);

        assertTrue(Files.exists(logFile), "migrations.log should be created");
        String content = Files.readString(logFile);
        assertTrue(content.contains("1 → 2"), "log should record the version step");
    }

    @Test
    void multiStepMigrationWritesMultipleLogEntries(@TempDir Path tempDir) throws Exception {
        Path logFile = tempDir.resolve("sub/migrations.log");
        MigrationManager manager = new MigrationManager(logFile);
        manager.register(tagMigration(1));
        manager.register(tagMigration(2));

        manager.migrateIfNeeded(propsAtVersion(1), 3);

        String content = Files.readString(logFile);
        assertTrue(content.contains("1 → 2"));
        assertTrue(content.contains("2 → 3"));
    }

    @Test
    void noLogFileWrittenWhenAlreadyAtTargetVersion(@TempDir Path tempDir) throws Exception {
        Path logFile = tempDir.resolve("migrations.log");
        MigrationManager manager = new MigrationManager(logFile);

        manager.migrateIfNeeded(propsAtVersion(1), 1);

        assertFalse(Files.exists(logFile), "log should not be created when no migration runs");
    }
}
