// frontend/src/lib/models/__tests__/GitConfigModel.test.ts
// #region GitConfigModelTests [C:3] [TYPE Module] [SEMANTICS test,model,git-config]
// @BRIEF L1 unit tests for GitConfigModel @INVARIANT guarantees — no DOM render.
// @RELATION BINDS_TO -> [GitConfigModel]
// @TEST_INVARIANT: loadConfigs-populates-list -> VERIFIED_BY: [test_loadConfigs_sets_configs_from_api]
// @TEST_INVARIANT: loadConfigs-loading-flag -> VERIFIED_BY: [test_loadConfigs_sets_loading_true_then_false]
// @TEST_INVARIANT: loadConfigs-error-handling -> VERIFIED_BY: [test_loadConfigs_handles_api_error]
// @TEST_INVARIANT: selectConfig-sets-id -> VERIFIED_BY: [test_selectConfig_sets_selectedConfig]
// @TEST_INVARIANT: startCreate-resets-form -> VERIFIED_BY: [test_startCreate_resets_form_state]
// @TEST_INVARIANT: startEdit-populates-form -> VERIFIED_BY: [test_startEdit_populates_form_from_config]
// @TEST_INVARIANT: cancelEdit-resets-state -> VERIFIED_BY: [test_cancelEdit_resets_to_initial_state]
// @TEST_INVARIANT: saveConfig-creates-POST -> VERIFIED_BY: [test_saveConfig_creates_new_config]
// @TEST_INVARIANT: saveConfig-updates-PUT -> VERIFIED_BY: [test_saveConfig_updates_existing_config]
// @TEST_INVARIANT: saveConfig-error -> VERIFIED_BY: [test_saveConfig_handles_error]
// @TEST_INVARIANT: deleteConfig-sends-DELETE -> VERIFIED_BY: [test_deleteConfig_sends_delete_request]
// @TEST_INVARIANT: testConnection-sends-POST -> VERIFIED_BY: [test_testConnection_sends_post_and_stores_result]
// @TEST_INVARIANT: loadGiteaRepos-fetches -> VERIFIED_BY: [test_loadGiteaRepos_fetches_repos]
// @TEST_INVARIANT: createRemoteRepo-sends-POST -> VERIFIED_BY: [test_createRemoteRepo_sends_post]
// @TEST_INVARIANT: deleteRemoteRepo-sends-DELETE -> VERIFIED_BY: [test_deleteRemoteRepo_sends_delete]
import { describe, it, expect, vi, beforeEach } from 'vitest';

// Mock gitService
vi.mock('../../../services/gitService.js', () => ({
  gitService: {
    getConfigs: vi.fn(),
    createConfig: vi.fn(),
    updateConfig: vi.fn(),
    deleteConfig: vi.fn(),
    testConnection: vi.fn(),
    listGiteaRepositories: vi.fn(),
    createGiteaRepository: vi.fn(),
    deleteGiteaRepository: vi.fn(),
  },
}));

// Mock toasts
vi.mock('$lib/toasts.svelte.js', () => ({
  addToast: vi.fn(),
}));

// Mock i18n
const mockTranslations = {
  settings: {
    connection_success: 'Connection successful',
    connection_failed_short: 'Connection failed',
    git_config_updated: 'Git configuration updated',
    git_config_saved: 'Git configuration saved',
    git_config_deleted: 'Git configuration deleted',
  },
  git: {},
  common: {},
  nav: {},
  llm: {},
};
vi.mock('$lib/i18n/index.svelte.js', () => ({
  t: {
    subscribe: (fn: (v: Record<string, unknown>) => void) => {
      fn(mockTranslations);
      return () => {};
    },
  },
  getT: vi.fn(() => mockTranslations),
  _: vi.fn((key: string) => key),
}));

import { GitConfigModel } from '../GitConfigModel.svelte.ts';
import { gitService } from '../../../services/gitService.js';
import { addToast } from '$lib/toasts.svelte.js';

describe('GitConfigModel — L1 invariants (no render)', () => {
  let model: GitConfigModel;

  beforeEach(() => {
    vi.clearAllMocks();
    model = new GitConfigModel();
  });

  // ═══════════════════════════════════════════════════════════════
  // loadConfigs
  // ═══════════════════════════════════════════════════════════════
  describe('loadConfigs', () => {
    it('sets configs from API', async () => {
      const mockConfigs = [{ id: 'c1', name: 'Test', provider: 'GITHUB' }];
      vi.mocked(gitService.getConfigs).mockResolvedValueOnce(mockConfigs);

      await model.loadConfigs();

      expect(gitService.getConfigs).toHaveBeenCalledOnce();
      expect(model.configs).toEqual(mockConfigs);
    });

    it('sets configsLoading true then false', async () => {
      vi.mocked(gitService.getConfigs).mockImplementation(() => new Promise((r) => setTimeout(r, 10)));

      const promise = model.loadConfigs();
      expect(model.configsLoading).toBe(true);
      await promise;
      expect(model.configsLoading).toBe(false);
    });

    it('handles API error', async () => {
      vi.mocked(gitService.getConfigs).mockRejectedValueOnce(new Error('Network error'));

      await model.loadConfigs();

      expect(model.configsError).toBe('Network error');
      expect(model.configs).toEqual([]);
      expect(addToast).toHaveBeenCalledWith('Network error', 'error');
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // selectConfig
  // ═══════════════════════════════════════════════════════════════
  describe('selectConfig', () => {
    it('sets selectedConfig', () => {
      model.selectConfig('conf-1');
      expect(model.selectedConfigId).toBe('conf-1');
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // startCreate
  // ═══════════════════════════════════════════════════════════════
  describe('startCreate', () => {
    it('resets form state to defaults', () => {
      // Set non-default state first
      model.isEditing = true;
      model.editingConfigId = 'old-id';
      model.formData = { name: 'Old', provider: 'GITLAB', url: 'https://old.com', pat: 'xxx', default_repository: 'org/repo', default_branch: 'dev' };

      model.startCreate();

      expect(model.isEditing).toBe(false);
      expect(model.editingConfigId).toBeNull();
      expect(model.formData).toEqual({
        name: '',
        provider: 'GITHUB',
        url: 'https://github.com',
        pat: '',
        default_repository: '',
        default_branch: 'main',
      });
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // startEdit
  // ═══════════════════════════════════════════════════════════════
  describe('startEdit', () => {
    it('populates form from config', () => {
      model.configs = [
        { id: 'c1', name: 'Prod', provider: 'GITHUB', url: 'https://github.com/org', pat: 'tok_xxx', default_repository: 'my-org/my-repo', default_branch: 'main' },
      ];

      model.startEdit('c1');

      expect(model.isEditing).toBe(true);
      expect(model.editingConfigId).toBe('c1');
      expect(model.formData).toEqual({
        name: 'Prod',
        provider: 'GITHUB',
        url: 'https://github.com/org',
        pat: 'tok_xxx',
        default_repository: 'my-org/my-repo',
        default_branch: 'main',
      });
    });

    it('does nothing for unknown id', () => {
      model.configs = [{ id: 'c1', name: 'Prod' }];
      model.startEdit('unknown');
      expect(model.isEditing).toBe(false);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // cancelEdit
  // ═══════════════════════════════════════════════════════════════
  describe('cancelEdit', () => {
    it('resets to initial state', () => {
      model.isEditing = true;
      model.editingConfigId = 'c1';
      model.formData.name = 'Dirty';

      model.cancelEdit();

      expect(model.isEditing).toBe(false);
      expect(model.editingConfigId).toBeNull();
      expect(model.formData.name).toBe('');
      expect(model.formData.provider).toBe('GITHUB');
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // saveConfig
  // ═══════════════════════════════════════════════════════════════
  describe('saveConfig', () => {
    it('creates new config (POST)', async () => {
      const saved = { id: 'new-1', name: 'New', provider: 'GITHUB', url: 'https://github.com', pat: 'tok', status: 'CONNECTED' };
      vi.mocked(gitService.createConfig).mockResolvedValueOnce(saved);
      const formSnapshot = { name: 'New', provider: 'GITHUB', url: 'https://github.com', pat: 'tok', default_repository: '', default_branch: 'main' };
      model.formData = { ...formSnapshot };

      await model.saveConfig();

      expect(gitService.createConfig).toHaveBeenCalledWith(formSnapshot);
      expect(gitService.updateConfig).not.toHaveBeenCalled();
      expect(model.configs).toContainEqual(saved);
      expect(model.isEditing).toBe(false);
      expect(addToast).toHaveBeenCalledWith('Git configuration saved', 'success');
    });

    it('updates existing config (PUT)', async () => {
      model.configs = [{ id: 'c1', name: 'Old', provider: 'GITHUB', url: 'https://github.com', pat: 'tok', status: 'CONNECTED' }];
      model.isEditing = true;
      model.editingConfigId = 'c1';
      const formSnapshot = { name: 'Updated', provider: 'GITHUB', url: 'https://github.com', pat: 'tok', default_repository: '', default_branch: 'main' };
      model.formData = { ...formSnapshot };
      const updated = { id: 'c1', name: 'Updated', provider: 'GITHUB', url: 'https://github.com', pat: 'tok', status: 'CONNECTED' };
      vi.mocked(gitService.updateConfig).mockResolvedValueOnce(updated);

      await model.saveConfig();

      expect(gitService.updateConfig).toHaveBeenCalledWith('c1', formSnapshot);
      expect(gitService.createConfig).not.toHaveBeenCalled();
      expect(model.configs[0].name).toBe('Updated');
      expect(addToast).toHaveBeenCalledWith('Git configuration updated', 'success');
    });

    it('handles error', async () => {
      vi.mocked(gitService.createConfig).mockRejectedValueOnce(new Error('Save failed'));

      await model.saveConfig();

      expect(model.saveError).toBe('Save failed');
      expect(addToast).toHaveBeenCalledWith('Save failed', 'error');
      expect(model.saving).toBe(false);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // deleteConfig
  // ═══════════════════════════════════════════════════════════════
  describe('deleteConfig', () => {
    it('sends DELETE request and removes from list', async () => {
      model.configs = [
        { id: 'c1', name: 'C1' },
        { id: 'c2', name: 'C2' },
      ];
      vi.mocked(gitService.deleteConfig).mockResolvedValueOnce({});

      await model.deleteConfig('c1');

      expect(gitService.deleteConfig).toHaveBeenCalledWith('c1');
      expect(model.configs).toHaveLength(1);
      expect(model.configs[0].id).toBe('c2');
      expect(addToast).toHaveBeenCalledWith('Git configuration deleted', 'success');
    });

    it('clears gitea state when deleting selected config', async () => {
      model.selectedGiteaConfigId = 'gitea-1';
      model.giteaRepos = [{ name: 'repo1' }];
      vi.mocked(gitService.deleteConfig).mockResolvedValueOnce({});

      await model.deleteConfig('gitea-1');

      expect(model.selectedGiteaConfigId).toBe('');
      expect(model.giteaRepos).toEqual([]);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // testConnection
  // ═══════════════════════════════════════════════════════════════
  describe('testConnection', () => {
    it('sends POST and stores result on success', async () => {
      vi.mocked(gitService.testConnection).mockResolvedValueOnce({ status: 'success' });
      model.formData = { name: 'Test', provider: 'GITHUB', url: 'https://github.com', pat: 'tok', default_repository: '', default_branch: 'main' };

      await model.testConnection();

      expect(gitService.testConnection).toHaveBeenCalledWith(expect.objectContaining({
        name: 'Test',
        config_id: null,
      }));
      expect(model.connectionTestResult).toEqual({ status: 'success' });
      expect(addToast).toHaveBeenCalledWith('Connection successful', 'success');
    });

    it('includes config_id when editing', async () => {
      model.isEditing = true;
      model.editingConfigId = 'c1';
      vi.mocked(gitService.testConnection).mockResolvedValueOnce({ status: 'success' });

      await model.testConnection();

      expect(gitService.testConnection).toHaveBeenCalledWith(expect.objectContaining({
        config_id: 'c1',
      }));
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // loadGiteaRepos
  // ═══════════════════════════════════════════════════════════════
  describe('loadGiteaRepos', () => {
    it('fetches repos for selected config', async () => {
      const repos = [{ name: 'repo1', full_name: 'user/repo1' }];
      vi.mocked(gitService.listGiteaRepositories).mockResolvedValueOnce(repos);
      model.selectedGiteaConfigId = 'gitea-1';

      await model.loadGiteaRepos();

      expect(gitService.listGiteaRepositories).toHaveBeenCalledWith('gitea-1');
      expect(model.giteaRepos).toEqual(repos);
    });

    it('clears repos when no config selected', async () => {
      model.giteaRepos = [{ name: 'stale' }];
      model.selectedGiteaConfigId = '';

      await model.loadGiteaRepos();

      expect(model.giteaRepos).toEqual([]);
      expect(gitService.listGiteaRepositories).not.toHaveBeenCalled();
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // createRemoteRepo
  // ═══════════════════════════════════════════════════════════════
  describe('createRemoteRepo', () => {
    it('sends POST to create Gitea repo', async () => {
      model.selectedGiteaConfigId = 'gitea-1';
      model.newGiteaRepo = { name: 'new-repo', private: true, description: 'desc', auto_init: true, default_branch: 'main' };
      vi.mocked(gitService.createGiteaRepository).mockResolvedValueOnce({});
      vi.mocked(gitService.listGiteaRepositories).mockResolvedValueOnce([]);

      await model.createRemoteRepo();

      expect(gitService.createGiteaRepository).toHaveBeenCalledWith('gitea-1', {
        name: 'new-repo',
        private: true,
        description: 'desc',
        auto_init: true,
        default_branch: 'main',
      });
      expect(gitService.listGiteaRepositories).toHaveBeenCalled();
    });

    it('does nothing with empty name', async () => {
      model.newGiteaRepo.name = '  ';
      await model.createRemoteRepo();
      expect(gitService.createGiteaRepository).not.toHaveBeenCalled();
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // deleteRemoteRepo
  // ═══════════════════════════════════════════════════════════════
  describe('deleteRemoteRepo', () => {
    it('sends DELETE with parsed owner/repo', async () => {
      model.selectedGiteaConfigId = 'gitea-1';
      vi.mocked(gitService.deleteGiteaRepository).mockResolvedValueOnce({});
      vi.mocked(gitService.listGiteaRepositories).mockResolvedValueOnce([]);

      await model.deleteRemoteRepo({ full_name: 'my-org/my-repo', name: 'my-repo' });

      expect(gitService.deleteGiteaRepository).toHaveBeenCalledWith('gitea-1', 'my-org', 'my-repo');
    });

    it('handles repo without full_name (cannot determine owner, returns early)', async () => {
      model.selectedGiteaConfigId = 'gitea-1';

      await model.deleteRemoteRepo({ name: 'simple-repo' });

      expect(gitService.deleteGiteaRepository).not.toHaveBeenCalled();
      expect(addToast).toHaveBeenCalledWith('Cannot resolve repository owner/name', 'error');
    });
  });
});
// #endregion GitConfigModelTests
