import { DataSource } from 'typeorm';
import { Character } from '../../characters/entities/character.entity';
import { LocationConnection } from '../../world/entities/location-connection.entity';
import { LocationDefinition } from '../../world/entities/location-definition.entity';
import { seedVisibleVerticalSlice } from './vertical-slice.seed';

type Row = Record<string, unknown>;

const DEMO_CHARACTER_ID = '10000000-0000-4000-8000-000000000001';
const SOUTH_GATE_ID = '20000000-0000-4000-8000-000000000001';
const BURNED_ROAD_ID = '20000000-0000-4000-8000-000000000002';

class InMemoryRepository {
  readonly rows: Row[] = [];
  readonly upsert = jest.fn(
    async (
      values: Row | Row[],
      conflictPaths: string[] | { conflictPaths: string[] },
    ) => {
      const conflictKeys = Array.isArray(conflictPaths)
        ? conflictPaths
        : conflictPaths.conflictPaths;

      for (const value of Array.isArray(values) ? values : [values]) {
        const existing = this.rows.find((row) =>
          conflictKeys.every((key) => row[key] === value[key]),
        );

        if (existing) {
          Object.assign(existing, value);
        } else {
          this.rows.push({ ...value });
        }
      }
    },
  );
  readonly findOneBy = jest.fn(async (criteria: Row) =>
    this.rows.find((row) =>
      Object.entries(criteria).every(([key, value]) => row[key] === value),
    ),
  );
  readonly insert = jest.fn(async (value: Row) => {
    this.rows.push({ ...value });
  });
  readonly update = jest.fn(async (criteria: string | Row, value: Row) => {
    const row = this.rows.find((candidate) =>
      typeof criteria === 'string'
        ? candidate.id === criteria
        : Object.entries(criteria).every(
            ([key, expected]) => candidate[key] === expected,
          ),
    );

    if (row) {
      Object.assign(row, value);
    }
  });
}

function createDataSource(
  locationRepository: InMemoryRepository,
  connectionRepository: InMemoryRepository,
  characterRepository: InMemoryRepository,
): DataSource {
  return {
    getRepository: jest.fn((entity: unknown) => {
      if (entity === LocationDefinition) {
        return locationRepository;
      }
      if (entity === LocationConnection) {
        return connectionRepository;
      }
      if (entity === Character) {
        return characterRepository;
      }

      throw new Error('Unexpected repository');
    }),
  } as unknown as DataSource;
}

describe('seedVisibleVerticalSlice', () => {
  it('upserts the two locations and directed connections while inserting one stable demo character', async () => {
    const locationRepository = new InMemoryRepository();
    const connectionRepository = new InMemoryRepository();
    const characterRepository = new InMemoryRepository();
    const dataSource = createDataSource(
      locationRepository,
      connectionRepository,
      characterRepository,
    );

    await seedVisibleVerticalSlice(dataSource);
    Object.assign(characterRepository.rows[0], {
      currentLocationId: BURNED_ROAD_ID,
      currentHp: 57,
      experience: 39,
    });
    await seedVisibleVerticalSlice(dataSource);

    expect(connectionRepository.upsert).toHaveBeenCalledWith(
      expect.arrayContaining([
        expect.objectContaining({
          fromLocationId: SOUTH_GATE_ID,
          toLocationId: BURNED_ROAD_ID,
        }),
        expect.objectContaining({
          fromLocationId: BURNED_ROAD_ID,
          toLocationId: SOUTH_GATE_ID,
        }),
      ]),
      ['fromLocationId', 'toLocationId'],
    );
    expect(characterRepository.insert).toHaveBeenCalledWith(
      expect.objectContaining({ id: DEMO_CHARACTER_ID }),
    );
    expect(locationRepository.rows).toHaveLength(2);
    expect(locationRepository.rows).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          key: 'south-gate',
          artworkPath: '/images/backgrounds/Suedtor.png',
        }),
        expect.objectContaining({
          key: 'burned-road',
          artworkPath: '/images/backgrounds/Aschestrasse.png',
        }),
      ]),
    );
    expect(connectionRepository.rows).toHaveLength(2);
    expect(characterRepository.rows).toHaveLength(1);
    expect(characterRepository.rows[0]).toEqual(
      expect.objectContaining({
        currentLocationId: BURNED_ROAD_ID,
        currentHp: 57,
        experience: 39,
      }),
    );
  });

  it('preserves existing location IDs and uses them for the directed connections', async () => {
    const locationRepository = new InMemoryRepository();
    const connectionRepository = new InMemoryRepository();
    const characterRepository = new InMemoryRepository();
    const persistedSouthGateId = '30000000-0000-4000-8000-000000000001';
    locationRepository.rows.push({
      id: persistedSouthGateId,
      key: 'south-gate',
      name: 'Veraltetes Südtor',
    });
    const dataSource = createDataSource(
      locationRepository,
      connectionRepository,
      characterRepository,
    );

    await seedVisibleVerticalSlice(dataSource);

    expect(locationRepository.rows).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          id: persistedSouthGateId,
          key: 'south-gate',
          name: 'Südtor von Graufurt',
        }),
        expect.objectContaining({
          id: BURNED_ROAD_ID,
          key: 'burned-road',
        }),
      ]),
    );
    expect(connectionRepository.rows).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          fromLocationId: persistedSouthGateId,
          toLocationId: BURNED_ROAD_ID,
        }),
        expect.objectContaining({
          fromLocationId: BURNED_ROAD_ID,
          toLocationId: persistedSouthGateId,
        }),
      ]),
    );
    expect(characterRepository.rows).toEqual([
      expect.objectContaining({
        id: DEMO_CHARACTER_ID,
        currentLocationId: persistedSouthGateId,
      }),
    ]);
  });
});
